Group plans subs to all (#8394)

* Added subscriptions to all members when group subs

* Added unsub when group cancels

* Give user a subscription when they join a subbed group

* Removed subscription when user leaves or is removed from group

* Fixed linting issues:

* Added tests for users with a subscription being upgraded to group plan

* Added tests for checking if existing recurring user sub gets updated during group plan. Added better merging for plans

* Added test for existing gift subscriptions

* Added additional months to user when they have an existing recurring subscription and get upgraded to group sub

* Adds test for user who has cancelled with date termined in the future

* Added test to ensure date termined is reset

* Added tests for extra months carrying over

* Added test for gems bought field

* Add tests to for fields that should remain when upgrading

* Added test for all payment methods

* Added prevention for when a user joins a second group plan

* Fixed subscribing tests

* Separated group plan payment tests

* Added prevention of editing a user with a unlimited sub

* Add tests to ensure group keeps plan if they are in two and leave one

* Ensured users with two group plans do not get cancelled when on group plan is cancelled

* Ensured users without group sub are untouched when group cancels

* Fixed lint issues

* Added new emails

* Added fix for cron tests

* Add restore to stubbed methods

* Ensured cancelled group subscriptions are updated

* Changed group plan exist check to check for date terminated

* Updated you cannont delete active group message

* Removed description requirement

* Added upgrade group plan for Amazon payments

* Fixed lint issues

* Fixed broken tests

* Fixed user delete tests

* Fixed function calls

* Hid cancel button if user has group plan

* Hide difficulty from rewards

* Prevented add user functions to be called when group plan is cancelled

* Fixed merge issue

* Correctly displayed group price

* Added message when you are about to join canclled group plan

* Fixed linting issues

* Updated tests to have no redirect to homes

* Allowed leaving a group with a canceld subscription

* Fixed spelling issues

* Prevented user from changing leader with active sub

* Added payment details title to replace subscription title

* Ensured we do not count leader when displaying upcoming cost

* Prevented party tasks from being displayed twice

* Prevented cancelling and already cancelled sub

* Fixed styles of subscriptions

* Added more specific mystery item tests

* Fixed test to refer to leader

* Extended test range to account for short months

* Fixed merge conflicts

* Updated yarn file

* Added missing locales

* Trigger notification

* Removed yarn

* Fixed locales

* Fixed scope mispelling

* Fixed line endings

* Removed extra advanced options from rewards

* Prevent group leader from leaving an active group plan

* Fixed issue with extra months applied to cancelled group plan

* Ensured member count is calculated when updatedGroupPlan

* Updated amazon payment method constant name

* Added comment to cancel sub user method

* Fixed smantic issues

* Added unite test for user isSubscribed and hasNotCancelled

* Add tests for isSubscribed and hasNotCanceled

* Changed default days remaining to 2 days for group plans

* Fixed logic with adding canceled notice to group invite
This commit is contained in:
Keith Holliday
2017-03-06 15:09:50 -07:00
committed by GitHub
parent 03a1d61c08
commit be60fb0635
33 changed files with 2128 additions and 668 deletions
@@ -28,7 +28,7 @@ angular.module('habitrpg')
};
$scope.newGroupIsReady = function () {
return $scope.newGroup.name && $scope.newGroup.description;
return !!$scope.newGroup.name;
};
$scope.createGroup = function () {
@@ -40,6 +40,10 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
}
$scope.join = function (group) {
if (group.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
return;
}
var groupId = group._id;
// If we don't have the _id property, we are joining from an invitation
@@ -65,6 +65,12 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
})
.then(function (response) {
var tasks = response.data.data;
$scope.group['habits'] = [];
$scope.group['dailys'] = [];
$scope.group['todos'] = [];
$scope.group['rewards'] = [];
tasks.forEach(function (element, index, array) {
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
$scope.group[element.type + 's'].unshift(element);
@@ -119,6 +125,10 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
};
$scope.join = function (party) {
if (party.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
return;
}
Groups.Group.join(party.id)
.then(function (response) {
$rootScope.party = $scope.group = response.data.data;
@@ -1,78 +1,78 @@
'use strict';
(function(){
angular
.module('habitrpg')
.directive('taskList', taskList);
taskList.$inject = [
'$state',
'User',
'$rootScope',
];
function taskList($state, User, $rootScope) {
return {
restrict: 'EA',
templateUrl: 'templates/task-list.html',
transclude: true,
scope: true,
// scope: {
// taskList: '=list',
// list: '=listDetails',
// obj: '=object',
// user: "=",
// },
link: function($scope, element, attrs) {
// @TODO: The use of scope with tasks is incorrect. We need to fix all task ctrls to use directives/services
// $scope.obj = {};
function setObj (obj, force) {
if (!force && ($scope.obj || scope.obj !== {} || !obj)) return;
$scope.obj = obj;
setUpGroupedList();
setUpTaskWatch();
}
$rootScope.$on('obj-updated', function (event, obj) {
setObj(obj, true);
});
function setUpGroupedList () {
if (!$scope.obj) return;
$scope.groupedList = {};
['habit', 'daily', 'todo', 'reward'].forEach(function (listType) {
groupTasksByChallenge($scope.obj[listType + 's'], listType);
});
}
setUpGroupedList();
function groupTasksByChallenge (taskList, type) {
$scope.groupedList[type] = _.groupBy(taskList, 'challenge.shortName');
};
function setUpTaskWatch () {
if (!$scope.obj) return;
$scope.$watch(function () { return $scope.obj.tasksOrder; }, function () {
setUpGroupedList();
}, true);
}
setUpTaskWatch();
$scope.getTaskList = function (list, taskList, obj) {
setObj(obj);
if (!$scope.obj) return [];
if (taskList) return taskList;
return $scope.obj[list.type+'s'];
};
$scope.showNormalList = function () {
return !$state.includes("options.social.challenges") && !User.user.preferences.tasks.groupByChallenge;
};
$scope.showChallengeList = function () {
return $state.includes("options.social.challenges");
};
}
}
}
}());
'use strict';
(function(){
angular
.module('habitrpg')
.directive('taskList', taskList);
taskList.$inject = [
'$state',
'User',
'$rootScope',
];
function taskList($state, User, $rootScope) {
return {
restrict: 'EA',
templateUrl: 'templates/task-list.html',
transclude: true,
scope: true,
// scope: {
// taskList: '=list',
// list: '=listDetails',
// obj: '=object',
// user: "=",
// },
link: function($scope, element, attrs) {
// @TODO: The use of scope with tasks is incorrect. We need to fix all task ctrls to use directives/services
// $scope.obj = {};
function setObj (obj, force) {
if (!force && ($scope.obj || $scope.obj !== {} || !obj)) return;
$scope.obj = obj;
setUpGroupedList();
setUpTaskWatch();
}
$rootScope.$on('obj-updated', function (event, obj) {
setObj(obj, true);
});
function setUpGroupedList () {
if (!$scope.obj) return;
$scope.groupedList = {};
['habit', 'daily', 'todo', 'reward'].forEach(function (listType) {
groupTasksByChallenge($scope.obj[listType + 's'], listType);
});
}
setUpGroupedList();
function groupTasksByChallenge (taskList, type) {
$scope.groupedList[type] = _.groupBy(taskList, 'challenge.shortName');
};
function setUpTaskWatch () {
if (!$scope.obj) return;
$scope.$watch(function () { return $scope.obj.tasksOrder; }, function () {
setUpGroupedList();
}, true);
}
setUpTaskWatch();
$scope.getTaskList = function (list, taskList, obj) {
setObj(obj);
if (!$scope.obj) return [];
if (taskList) return taskList;
return $scope.obj[list.type+'s'];
};
$scope.showNormalList = function () {
return !$state.includes("options.social.challenges") && !User.user.preferences.tasks.groupByChallenge;
};
$scope.showChallengeList = function () {
return $state.includes("options.social.challenges");
};
}
}
}
}());
@@ -22,10 +22,10 @@ function($rootScope, User, $http, Content) {
sub = sub && Content.subscriptionBlocks[sub];
var amount = // 500 = $5
sub ? sub.price*100
: data.gift && data.gift.type=='gems' ? data.gift.gems.amount/4*100
: 500;
var amount = 500;// 500 = $5
if (sub) amount = sub.price * 100;
if (data.gift && data.gift.type=='gems') amount = data.gift.gems.amount / 4 * 100;
if (data.group) amount = (sub.price + 3 * (data.group.memberCount - 1)) * 100;
StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY,
+262 -258
View File
@@ -1,258 +1,262 @@
{
"tavern": "Tavern Chat",
"innCheckOut": "Check Out of Inn",
"innCheckIn": "Rest in the Inn",
"innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
"innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
"lfgPosts": "Looking for Group (Party Wanted) Posts",
"tutorial": "Tutorial",
"glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
"wiki": "Wiki",
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
"reportAP": "Report a Problem",
"requestAF": "Request a Feature",
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
"dataTool": "Data Display Tool",
"resources": "Resources",
"askQuestionNewbiesGuild": "Ask a Question (Habitica Help guild)",
"tavernAlert1": "To report a bug, visit",
"tavernAlert2": "the Report a Bug Guild",
"moderatorIntro1": "Tavern and guild moderators are: ",
"communityGuidelines": "Community Guidelines",
"communityGuidelinesRead1": "Please read our",
"communityGuidelinesRead2": "before chatting.",
"party": "Party",
"createAParty": "Create A Party",
"updatedParty": "Party settings updated.",
"noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
"LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
"wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
"joinExistingParty": "Join Someone Else's Party",
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> before you can start a quest!",
"create": "Create",
"userId": "User ID",
"invite": "Invite",
"leave": "Leave",
"invitedTo": "Invited to <%= name %>",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
"invitationAcceptedHeader": "Your Invitation has been Accepted",
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
"joinNewParty": "Join New Party",
"declineInvitation": "Decline Invitation",
"partyLoading1": "Your party is being summoned. Please wait...",
"partyLoading2": "Your party is coming in from battle. Please wait...",
"partyLoading3": "Your party is gathering. Please wait...",
"partyLoading4": "Your party is materializing. Please wait...",
"systemMessage": "System Message",
"newMsg": "New message in \"<%= name %>\"",
"chat": "Chat",
"sendChat": "Send Chat",
"toolTipMsg": "Fetch Recent Messages",
"sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
"syncPartyAndChat": "Sync Party and Chat",
"guildBankPop1": "Guild Bank",
"guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
"guildGems": "Guild Gems",
"editGroup": "Edit Group",
"newGroupName": "<%= groupType %> Name",
"groupName": "Group Name",
"groupLeader": "Group Leader",
"groupID": "Group ID",
"groupDescr": "Description shown in public Guilds list (Markdown OK)",
"logoUrl": "Logo URL",
"assignLeader": "Assign Group Leader",
"members": "Members",
"partyList": "Order for party members in header",
"banTip": "Boot Member",
"moreMembers": "more members",
"invited": "Invited",
"leaderMsg": "Message from group leader (Markdown OK)",
"name": "Name",
"description": "Description",
"public": "Public",
"inviteOnly": "Invite Only",
"gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
"search": "Search",
"publicGuilds": "Public Guilds",
"createGuild": "Create Guild",
"guild": "Guild",
"guilds": "Guilds",
"guildsLink": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
"sureKick": "Do you really want to remove this member from the party/guild?",
"optionalMessage": "Optional message",
"yesRemove": "Yes, remove them",
"foreverAlone": "Can't like your own message. Don't be that person.",
"sortLevel": "Sort by level",
"sortRandom": "Sort randomly",
"sortPets": "Sort by number of pets",
"sortName": "Sort by avatar name",
"sortBackgrounds": "Sort by background",
"sortHabitrpgJoined": "Sort by Habitica date joined",
"sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
"ascendingSort": "Sort Ascending",
"descendingSort": "Sort Descending",
"confirmGuild": "Create Guild for 4 Gems?",
"leaveGroupCha": "Leave Guild challenges and...",
"confirm": "Confirm",
"leaveGroup": "Leave Guild?",
"leavePartyCha": "Leave party challenges and...",
"leaveParty": "Leave party?",
"sendPM": "Send private message",
"send": "Send",
"messageSentAlert": "Message sent",
"pmHeading": "Private message to <%= name %>",
"pmsMarkedRead": "Your private messages have been marked as read",
"possessiveParty": "<%= name %>'s Party",
"clearAll": "Delete All Messages",
"confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
"optOutPopover": "Don't like private messages? Click to completely opt out",
"block": "Block",
"unblock": "Un-block",
"pm-reply": "Send a reply",
"inbox": "Inbox",
"messageRequired": "A message is required.",
"toUserIDRequired": "A User ID is required",
"gemAmountRequired": "A number of gems is required",
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
"privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!",
"privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
"abuseFlag": "Report violation of Community Guidelines",
"abuseFlagModalHeading": "Report <%= name %> for violation?",
"abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
"abuseFlagModalButton": "Report Violation",
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
"abuseAlreadyReported": "You have already reported this message.",
"needsText": "Please type a message.",
"needsTextPlaceholder": "Type your message here.",
"copyMessageAsToDo": "Copy message as To-Do",
"messageAddedAsToDo": "Message copied as To-Do.",
"messageWroteIn": "<%= user %> wrote in <%= group %>",
"taskFromInbox": "<%= from %> wrote '<%= message %>'",
"taskTextFromInbox": "Message from <%= from %>",
"msgPreviewHeading": "Message Preview",
"leaderOnlyChallenges": "Only group leader can create challenges",
"sendGift": "Send Gift",
"inviteFriends": "Invite Friends",
"inviteByEmail": "Invite by Email",
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
"inviteFriendsNow": "Invite Friends Now",
"inviteFriendsLater": "Invite Friends Later",
"inviteAlertInfo": "If you have friends already using Habitica, invite them by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
"inviteExistUser": "Invite Existing Users",
"byColon": "By:",
"inviteNewUsers": "Invite New Users",
"sendInvitations": "Send Invitations",
"invitationsSent": "Invitations sent!",
"invitationSent": "Invitation sent!",
"inviteAlertInfo2": "Or share this link (copy/paste):",
"sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftCost": "Total: $<%= cost %> USD",
"sendGiftFromBalance": "From Balance",
"sendGiftPurchase": "Purchase",
"sendGiftMessagePlaceholder": "Personal message (optional)",
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
"battleWithFriends": "Battle Monsters With Friends",
"startPartyWithFriends": "Start a Party with your friends!",
"startAParty": "Start a Party",
"addToParty": "Add someone to your party",
"likePost": "Click if you like this post!",
"partyExplanation1": "Play Habitica with friends to stay accountable!",
"partyExplanation2": "Battle monsters and create Challenges!",
"partyExplanation3": "Invite friends now to earn a Quest Scroll!",
"wantToStartParty": "Do you want to start a party?",
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
"nameYourParty": "Name your new party!",
"partyEmpty": "You're the only one in your party. Invite your friends!",
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
"possessiveParty": "<%= name %>'s Party",
"requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.",
"partyUpName": "Party Up",
"partyOnName": "Party On",
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
"groupIdRequired": "\"groupId\" must be a valid UUID",
"groupNotFound": "Group not found or you don't have access.",
"groupTypesRequired": "You must supply a valid \"type\" query string.",
"questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
"cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
"memberCannotRemoveYourself": "You cannot remove yourself!",
"groupMemberNotFound": "User not found among group's members",
"mustBeGroupMember": "Must be member of the group.",
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
"inviteMissingEmail": "Missing email address in invite.",
"inviteMissingUuid": "Missing user id in invite",
"inviteMustNotBeEmpty": "Invite must not be empty.",
"partyMustbePrivate": "Parties must be private",
"userAlreadyInGroup": "User already in that group.",
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
"userAlreadyInvitedToGroup": "User already invited to that group.",
"userAlreadyPendingInvitation": "User already pending invitation.",
"userAlreadyInAParty": "User already in a party.",
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
"uuidsMustBeAnArray": "User ID invites must be an array.",
"emailsMustBeAnArray": "Email address invites must be an array.",
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
"newChatMessageTitle": "New message in <%= groupName %>",
"exportInbox": "Export Messages",
"exportInboxPopoverTitle": "Export your messages as HTML",
"exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
"to": "To:",
"from": "From:",
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
"groupHomeTitle": "Home",
"assignTask": "Assign Task",
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"claim": "Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task \"<%= taskText %>\" has been approved",
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
"approve": "Approve",
"approvalTitle": "<%= text %> for user: <%= userName %>",
"confirmTaskApproval": "Do you want to reward <%= username %> for completing this task?",
"groupSubscriptionPrice": "$9 every month + $3 a month for every additional group member",
"groupAdditionalUserCost": " +$3.00/month/user",
"groupBenefitsTitle": "How a group plan can help you",
"groupBenefitsDescription": "We've just launched the beta version of our group plans! Upgrading to a group plan unlocks some unique features to optimize the social side of Habitica.",
"groupBenefitOneTitle": "Create a shared task list",
"groupBenefitOneDescription": "Set up a shared task list for the group that everyone can easily view and edit.",
"groupBenefitTwoTitle": "Assign tasks to group members",
"groupBenefitTwoDescription": "Want a coworker to answer a critical email? Need your roommate to pick up the groceries? Just assign them the tasks you create, and they'll automatically appear in that person's task dashboard.",
"groupBenefitThreeTitle": "Claim a task that you are working on",
"groupBenefitThreeDescription": "Stake your claim on any group task with a simple click. Make it clear what everybody is working on!",
"groupBenefitFourTitle": "Mark tasks that require special approval",
"groupBenefitFourDescription": "Need to verify that a task really did get done before that user gets their rewards? Just adjust the approval settings for added control.",
"groupBenefitFiveTitle": "Chat privately with your group",
"groupBenefitFiveDescription": "Stay in the loop about important decisions in our easy-to-use chatroom!",
"createAGroup": "Create a Group",
"assignFieldPlaceholder": "Type a group member's profile name",
"cannotDeleteActiveGroup": "You cannot remove a group with an active subscription",
"groupTasksTitle": "Group Tasks List",
"approvalsTitle": "Tasks Awaiting Approval",
"upgradeTitle": "Upgrade",
"blankApprovalsDescription": "When your group completes tasks that need your approval, they'll appear here! Adjust approval requirement settings under task editing.",
"userIsClamingTask": "`<%= username %> has claimed \"<%= task %>\"`",
"approvalRequested": "Approval Requested",
"refreshApprovals": "Refresh Approvals",
"refreshGroupTasks": "Refresh Group Tasks",
"claimedBy": "\n\nClaimed by: <%= claimingUsers %>",
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
"confirmGuildPlanCreation": "Create this group?",
"onlyGroupLeaderCanInviteToGroupPlan": "Only the group leader can invite users to a group with a subscription.",
"remainOrLeaveChallenges": "req.query.keep must be either 'remain-in-challenges' or 'leave-challenges'"
}
{
"tavern": "Tavern Chat",
"innCheckOut": "Check Out of Inn",
"innCheckIn": "Rest in the Inn",
"innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
"innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
"lfgPosts": "Looking for Group (Party Wanted) Posts",
"tutorial": "Tutorial",
"glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
"wiki": "Wiki",
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
"reportAP": "Report a Problem",
"requestAF": "Request a Feature",
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
"dataTool": "Data Display Tool",
"resources": "Resources",
"askQuestionNewbiesGuild": "Ask a Question (Habitica Help guild)",
"tavernAlert1": "To report a bug, visit",
"tavernAlert2": "the Report a Bug Guild",
"moderatorIntro1": "Tavern and guild moderators are: ",
"communityGuidelines": "Community Guidelines",
"communityGuidelinesRead1": "Please read our",
"communityGuidelinesRead2": "before chatting.",
"party": "Party",
"createAParty": "Create A Party",
"updatedParty": "Party settings updated.",
"noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
"LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
"wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
"joinExistingParty": "Join Someone Else's Party",
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> before you can start a quest!",
"create": "Create",
"userId": "User ID",
"invite": "Invite",
"leave": "Leave",
"invitedTo": "Invited to <%= name %>",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
"invitationAcceptedHeader": "Your Invitation has been Accepted",
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
"joinNewParty": "Join New Party",
"declineInvitation": "Decline Invitation",
"partyLoading1": "Your party is being summoned. Please wait...",
"partyLoading2": "Your party is coming in from battle. Please wait...",
"partyLoading3": "Your party is gathering. Please wait...",
"partyLoading4": "Your party is materializing. Please wait...",
"systemMessage": "System Message",
"newMsg": "New message in \"<%= name %>\"",
"chat": "Chat",
"sendChat": "Send Chat",
"toolTipMsg": "Fetch Recent Messages",
"sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
"syncPartyAndChat": "Sync Party and Chat",
"guildBankPop1": "Guild Bank",
"guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
"guildGems": "Guild Gems",
"editGroup": "Edit Group",
"newGroupName": "<%= groupType %> Name",
"groupName": "Group Name",
"groupLeader": "Group Leader",
"groupID": "Group ID",
"groupDescr": "Description shown in public Guilds list (Markdown OK)",
"logoUrl": "Logo URL",
"assignLeader": "Assign Group Leader",
"members": "Members",
"partyList": "Order for party members in header",
"banTip": "Boot Member",
"moreMembers": "more members",
"invited": "Invited",
"leaderMsg": "Message from group leader (Markdown OK)",
"name": "Name",
"description": "Description",
"public": "Public",
"inviteOnly": "Invite Only",
"gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
"search": "Search",
"publicGuilds": "Public Guilds",
"createGuild": "Create Guild",
"guild": "Guild",
"guilds": "Guilds",
"guildsLink": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
"sureKick": "Do you really want to remove this member from the party/guild?",
"optionalMessage": "Optional message",
"yesRemove": "Yes, remove them",
"foreverAlone": "Can't like your own message. Don't be that person.",
"sortLevel": "Sort by level",
"sortRandom": "Sort randomly",
"sortPets": "Sort by number of pets",
"sortName": "Sort by avatar name",
"sortBackgrounds": "Sort by background",
"sortHabitrpgJoined": "Sort by Habitica date joined",
"sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
"ascendingSort": "Sort Ascending",
"descendingSort": "Sort Descending",
"confirmGuild": "Create Guild for 4 Gems?",
"leaveGroupCha": "Leave Guild challenges and...",
"confirm": "Confirm",
"leaveGroup": "Leave Guild?",
"leavePartyCha": "Leave party challenges and...",
"leaveParty": "Leave party?",
"sendPM": "Send private message",
"send": "Send",
"messageSentAlert": "Message sent",
"pmHeading": "Private message to <%= name %>",
"pmsMarkedRead": "Your private messages have been marked as read",
"possessiveParty": "<%= name %>'s Party",
"clearAll": "Delete All Messages",
"confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
"optOutPopover": "Don't like private messages? Click to completely opt out",
"block": "Block",
"unblock": "Un-block",
"pm-reply": "Send a reply",
"inbox": "Inbox",
"messageRequired": "A message is required.",
"toUserIDRequired": "A User ID is required",
"gemAmountRequired": "A number of gems is required",
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
"privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!",
"privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
"abuseFlag": "Report violation of Community Guidelines",
"abuseFlagModalHeading": "Report <%= name %> for violation?",
"abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
"abuseFlagModalButton": "Report Violation",
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
"abuseAlreadyReported": "You have already reported this message.",
"needsText": "Please type a message.",
"needsTextPlaceholder": "Type your message here.",
"copyMessageAsToDo": "Copy message as To-Do",
"messageAddedAsToDo": "Message copied as To-Do.",
"messageWroteIn": "<%= user %> wrote in <%= group %>",
"taskFromInbox": "<%= from %> wrote '<%= message %>'",
"taskTextFromInbox": "Message from <%= from %>",
"msgPreviewHeading": "Message Preview",
"leaderOnlyChallenges": "Only group leader can create challenges",
"sendGift": "Send Gift",
"inviteFriends": "Invite Friends",
"inviteByEmail": "Invite by Email",
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
"inviteFriendsNow": "Invite Friends Now",
"inviteFriendsLater": "Invite Friends Later",
"inviteAlertInfo": "If you have friends already using Habitica, invite them by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
"inviteExistUser": "Invite Existing Users",
"byColon": "By:",
"inviteNewUsers": "Invite New Users",
"sendInvitations": "Send Invitations",
"invitationsSent": "Invitations sent!",
"invitationSent": "Invitation sent!",
"inviteAlertInfo2": "Or share this link (copy/paste):",
"sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftCost": "Total: $<%= cost %> USD",
"sendGiftFromBalance": "From Balance",
"sendGiftPurchase": "Purchase",
"sendGiftMessagePlaceholder": "Personal message (optional)",
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
"battleWithFriends": "Battle Monsters With Friends",
"startPartyWithFriends": "Start a Party with your friends!",
"startAParty": "Start a Party",
"addToParty": "Add someone to your party",
"likePost": "Click if you like this post!",
"partyExplanation1": "Play Habitica with friends to stay accountable!",
"partyExplanation2": "Battle monsters and create Challenges!",
"partyExplanation3": "Invite friends now to earn a Quest Scroll!",
"wantToStartParty": "Do you want to start a party?",
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
"nameYourParty": "Name your new party!",
"partyEmpty": "You're the only one in your party. Invite your friends!",
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
"possessiveParty": "<%= name %>'s Party",
"requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.",
"partyUpName": "Party Up",
"partyOnName": "Party On",
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
"groupIdRequired": "\"groupId\" must be a valid UUID",
"groupNotFound": "Group not found or you don't have access.",
"groupTypesRequired": "You must supply a valid \"type\" query string.",
"questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
"cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
"memberCannotRemoveYourself": "You cannot remove yourself!",
"groupMemberNotFound": "User not found among group's members",
"mustBeGroupMember": "Must be member of the group.",
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
"inviteMissingEmail": "Missing email address in invite.",
"inviteMissingUuid": "Missing user id in invite",
"inviteMustNotBeEmpty": "Invite must not be empty.",
"partyMustbePrivate": "Parties must be private",
"userAlreadyInGroup": "User already in that group.",
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
"userAlreadyInvitedToGroup": "User already invited to that group.",
"userAlreadyPendingInvitation": "User already pending invitation.",
"userAlreadyInAParty": "User already in a party.",
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
"uuidsMustBeAnArray": "User ID invites must be an array.",
"emailsMustBeAnArray": "Email address invites must be an array.",
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
"newChatMessageTitle": "New message in <%= groupName %>",
"exportInbox": "Export Messages",
"exportInboxPopoverTitle": "Export your messages as HTML",
"exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
"to": "To:",
"from": "From:",
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
"groupHomeTitle": "Home",
"assignTask": "Assign Task",
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"claim": "Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task \"<%= taskText %>\" has been approved",
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
"approve": "Approve",
"approvalTitle": "<%= text %> for user: <%= userName %>",
"confirmTaskApproval": "Do you want to reward <%= username %> for completing this task?",
"groupSubscriptionPrice": "$9 every month + $3 a month for every additional group member",
"groupAdditionalUserCost": " +$3.00/month/user",
"groupBenefitsTitle": "How a group plan can help you",
"groupBenefitsDescription": "We've just launched the beta version of our group plans! Upgrading to a group plan unlocks some unique features to optimize the social side of Habitica.",
"groupBenefitOneTitle": "Create a shared task list",
"groupBenefitOneDescription": "Set up a shared task list for the group that everyone can easily view and edit.",
"groupBenefitTwoTitle": "Assign tasks to group members",
"groupBenefitTwoDescription": "Want a coworker to answer a critical email? Need your roommate to pick up the groceries? Just assign them the tasks you create, and they'll automatically appear in that person's task dashboard.",
"groupBenefitThreeTitle": "Claim a task that you are working on",
"groupBenefitThreeDescription": "Stake your claim on any group task with a simple click. Make it clear what everybody is working on!",
"groupBenefitFourTitle": "Mark tasks that require special approval",
"groupBenefitFourDescription": "Need to verify that a task really did get done before that user gets their rewards? Just adjust the approval settings for added control.",
"groupBenefitFiveTitle": "Chat privately with your group",
"groupBenefitFiveDescription": "Stay in the loop about important decisions in our easy-to-use chatroom!",
"createAGroup": "Create a Group",
"assignFieldPlaceholder": "Type a group member's profile name",
"cannotDeleteActiveGroup": "You cannot remove a group with an active subscription",
"groupTasksTitle": "Group Tasks List",
"approvalsTitle": "Tasks Awaiting Approval",
"upgradeTitle": "Upgrade",
"blankApprovalsDescription": "When your group completes tasks that need your approval, they'll appear here! Adjust approval requirement settings under task editing.",
"userIsClamingTask": "`<%= username %> has claimed \"<%= task %>\"`",
"approvalRequested": "Approval Requested",
"refreshApprovals": "Refresh Approvals",
"refreshGroupTasks": "Refresh Group Tasks",
"claimedBy": "\n\nClaimed by: <%= claimingUsers %>",
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
"confirmGuildPlanCreation": "Create this group?",
"onlyGroupLeaderCanInviteToGroupPlan": "Only the group leader can invite users to a group with a subscription.",
"remainOrLeaveChallenges": "req.query.keep must be either 'remain-in-challenges' or 'leave-challenges'",
"paymentDetails": "Payment Details",
"aboutToJoinCancelledGroupPlan": "You are about to join a group with a canceled plan. You will NOT receive a free subscription.",
"cannotChangeLeaderWithActiveGroupPlan": "You can not change the leader while the group has an active plan.",
"leaderCannotLeaveGroupWithActiveGroup": "A leader can not leave a group while the group has an active plan"
}
@@ -30,6 +30,12 @@ let subscriptionBlocks = {
price: 9,
quantity: 3, // Default quantity for Stripe - The same as having 3 user subscriptions
},
group_plan_auto: {
type: 'group',
months: 0,
price: 0,
quantity: 1,
},
};
each(subscriptionBlocks, function createKeys (b, k) {
+24 -5
View File
@@ -387,10 +387,13 @@ api.updateGroup = {
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
if (req.body.leader !== user._id && group.hasNotCancelled()) throw new NotAuthorized(res.t('cannotChangeLeaderWithActiveGroupPlan'));
_.assign(group, _.merge(group.toObject(), Group.sanitizeUpdate(req.body)));
let savedGroup = await group.save();
let response = Group.toJSONCleanChat(savedGroup, user);
// If the leader changed fetch new data, otherwise use authenticated user
if (response.leader !== user._id) {
let rawLeader = await User.findById(response.leader).select(nameFields).exec();
@@ -493,7 +496,10 @@ api.joinGroup = {
group.memberCount += 1;
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
if (group.hasNotCancelled()) {
await payments.addSubToGroupUser(user, group);
await group.updateGroupPlan();
}
let promises = [group.save(), user.save()];
@@ -676,7 +682,7 @@ api.leaveGroup = {
await group.leave(user, req.query.keep, req.body.keepChallenges);
if (group.purchased.plan && group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
_removeMessagesFromMember(user, group._id);
@@ -763,7 +769,10 @@ api.removeGroupMember = {
if (isInGroup) {
group.memberCount -= 1;
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
if (group.hasNotCancelled()) {
await group.updateGroupPlan(true);
await payments.cancelGroupSubscriptionForUser(member, group);
}
if (group.quest && group.quest.leader === member._id) {
group.quest.key = undefined;
@@ -831,7 +840,10 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
if (_.find(userToInvite.invitations.guilds, {id: group._id})) {
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup'));
}
userToInvite.invitations.guilds.push({id: group._id, name: group.name, inviter: inviter._id});
let guildInvite = {id: group._id, name: group.name, inviter: inviter._id};
if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
userToInvite.invitations.guilds.push(guildInvite);
} else if (group.type === 'party') {
if (userToInvite.invitations.party.id) {
throw new NotAuthorized(res.t('userAlreadyPendingInvitation'));
@@ -844,7 +856,9 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty'));
}
userToInvite.invitations.party = {id: group._id, name: group.name, inviter: inviter._id};
let partyInvite = {id: group._id, name: group.name, inviter: inviter._id};
if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
userToInvite.invitations.party = partyInvite;
}
let groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
@@ -907,10 +921,15 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res);
} else {
userReturnInfo = invite.email;
let cancelledPlan = false;
if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true;
const groupQueryString = JSON.stringify({
id: group._id,
inviter: inviter._id,
sentAt: Date.now(), // so we can let it expire
cancelledPlan,
});
let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`;
+1 -1
View File
@@ -209,7 +209,7 @@ api.deleteUser = {
}
let types = ['party', 'guilds'];
let groupFields = basicGroupFields.concat(' leader memberCount');
let groupFields = basicGroupFields.concat(' leader memberCount purchased');
let groupsUserIsMemberOf = await Group.getGroups({user, types, groupFields});
+33 -6
View File
@@ -3,6 +3,7 @@ import nconf from 'nconf';
import Bluebird from 'bluebird';
import moment from 'moment';
import cc from 'coupon-code';
import uuid from 'uuid';
import common from '../../common';
import {
@@ -38,6 +39,7 @@ api.constants = {
SELLER_NOTE: 'Habitica Payment',
SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
SELLER_NOTE_GROUP_NEW_MEMBER: 'Habitica Group Plan New Member',
STORE_NAME: 'Habitica',
GIFT_TYPE_GEMS: 'gems',
@@ -45,8 +47,8 @@ api.constants = {
METHOD_BUY_GEMS: 'buyGems',
METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD_AMAZON: 'Amazon Payments',
PAYMENT_METHOD_AMAZON_GIFT: 'Amazon Payments (Gift)',
PAYMENT_METHOD: 'Amazon Payments',
PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};
api.getTokenInfo = Bluebird.promisify(amzPayment.api.getTokenInfo, {context: amzPayment.api});
@@ -138,7 +140,7 @@ api.checkout = async function checkout (options = {}) {
let data = {
user,
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: this.constants.PAYMENT_METHOD,
headers,
};
@@ -146,7 +148,7 @@ api.checkout = async function checkout (options = {}) {
if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) method = this.constants.METHOD_CREATE_SUBSCRIPTION;
gift.member = await User.findById(gift ? gift.uuid : undefined).exec();
data.gift = gift;
data.paymentMethod = this.constants.PAYMENT_METHOD_AMAZON_GIFT;
data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT;
}
await payments[method](data);
@@ -209,7 +211,7 @@ api.cancelSubscription = async function cancelSubscription (options = {}) {
user,
groupId,
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: this.constants.PAYMENT_METHOD,
headers,
});
};
@@ -281,11 +283,36 @@ api.subscribe = async function subscribe (options) {
await payments.createSubscription({
user,
customerId: billingAgreementId,
paymentMethod: this.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: this.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
};
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
// @TODO: Can we get this from the content plan?
let priceForNewMember = 3;
// @TODO: Prorate?
return this.authorizeOnBillingAgreement({
AmazonBillingAgreementId: group.purchased.plan.customerId,
AuthorizationReferenceId: uuid.v4().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: this.constants.CURRENCY_CODE,
Amount: priceForNewMember,
},
SellerAuthorizationNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
SellerOrderAttributes: {
SellerOrderId: uuid.v4(),
StoreName: this.constants.STORE_NAME,
},
});
};
module.exports = api;
+3 -1
View File
@@ -23,7 +23,9 @@ const PLATFORM_MAP = Object.freeze({
'habitica-android': 'Android',
});
let amplitude = new Amplitude(AMPLIUDE_TOKEN);
let amplitude;
if (AMPLIUDE_TOKEN) amplitude = new Amplitude(AMPLIUDE_TOKEN);
let ga = googleAnalytics(GA_TOKEN);
let _lookUpItemName = (itemKey) => {
+16 -15
View File
@@ -75,24 +75,21 @@ function grantEndOfTheMonthPerks (user, now) {
}
function removeTerminatedSubscription (user) {
// If subscription's termination date has arrived
let plan = user.purchased.plan;
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
_.merge(plan, {
planId: null,
customerId: null,
paymentMethod: null,
});
_.merge(plan, {
planId: null,
customerId: null,
paymentMethod: null,
});
_.merge(plan.consecutive, {
count: 0,
offset: 0,
gemCapExtra: 0,
});
_.merge(plan.consecutive, {
count: 0,
offset: 0,
gemCapExtra: 0,
});
user.markModified('purchased.plan');
}
user.markModified('purchased.plan');
}
function performSleepTasks (user, tasksByType, now) {
@@ -195,11 +192,15 @@ export function cron (options = {}) {
if (user.purchased && user.purchased.plan && !moment(user.purchased.plan.dateUpdated).startOf('month').isSame(moment().startOf('month'))) {
user.purchased.plan.gemsBought = 0;
}
if (user.isSubscribed()) {
grantEndOfTheMonthPerks(user, now);
if (!CRON_SAFE_MODE) removeTerminatedSubscription(user);
}
let plan = user.purchased.plan;
let userHasTerminatedSubscription = plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date());
if (!CRON_SAFE_MODE && userHasTerminatedSubscription) removeTerminatedSubscription(user);
// Login Incentives
user.loginIncentives++;
awardLoginIncentives(user);
+177 -24
View File
@@ -11,19 +11,21 @@ import {
model as Group,
basicFields as basicGroupFields,
} from '../models/group';
import { model as User } from '../models/user';
import {
NotAuthorized,
NotFound,
} from './errors';
import slack from './slack';
import nconf from 'nconf';
import stripeModule from 'stripe';
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
let api = {};
api.constants = {
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
};
function revealMysteryItems (user) {
_.each(shared.content.gear.flat, function findMysteryItems (item) {
if (
@@ -44,6 +46,158 @@ function _dateDiff (earlyDate, lateDate) {
return moment(lateDate).diff(earlyDate, 'months', true);
}
/**
* Add a subscription to members of a group
*
* @param group The Group Model that is subscribed to a group plan
*
* @return undefined
*/
api.addSubscriptionToGroupUsers = async function addSubscriptionToGroupUsers (group) {
let members;
if (group.type === 'guild') {
members = await User.find({guilds: group._id}).select('_id purchased').exec();
} else {
members = await User.find({'party._id': group._id}).select('_id purchased').exec();
}
let promises = members.map((member) => {
return this.addSubToGroupUser(member, group);
});
await Promise.all(promises);
};
/**
* Add a subscription to a new member of a group
*
* @param member The new member of the group
*
* @return undefined
*/
api.addSubToGroupUser = async function addSubToGroupUser (member, group) {
let customerIdsToIgnore = [this.constants.GROUP_PLAN_CUSTOMER_ID, this.constants.UNLIMITED_CUSTOMER_ID];
let data = {
user: {},
sub: {
key: 'group_plan_auto',
},
customerId: 'group-plan',
paymentMethod: 'Group Plan',
headers: {},
};
let plan = {
planId: 'group_plan_auto',
customerId: 'group-plan',
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: 'groupPlan',
extraMonths: 0,
dateTerminated: null,
lastBillingDate: null,
dateCreated: new Date(),
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
if (member.isSubscribed()) {
let memberPlan = member.purchased.plan;
let customerHasCancelledGroupPlan = memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID && !member.hasNotCancelled();
if (customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1 && !customerHasCancelledGroupPlan) return;
if (member.hasNotCancelled()) await member.cancelSubscription();
let today = new Date();
plan = _.clone(member.purchased.plan.toObject());
let extraMonths = Number(plan.extraMonths);
if (plan.dateTerminated) extraMonths += _dateDiff(today, plan.dateTerminated);
_(plan).merge({ // override with these values
planId: 'group_plan_auto',
customerId: 'group-plan',
dateUpdated: today,
paymentMethod: 'groupPlan',
extraMonths,
dateTerminated: null,
lastBillingDate: null,
owner: member._id,
}).defaults({ // allow non-override if a plan was previously used
gemsBought: 0,
dateCreated: today,
mysteryItems: [],
}).value();
}
member.purchased.plan = plan;
data.user = member;
await this.createSubscription(data);
let leader = await User.findById(group.leader).exec();
txnEmail(data.user, 'group-member-joining', [
{name: 'LEADER', content: leader.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
};
/**
* Cancels subscriptions of members of a group
*
* @param group The Group Model that is cancelling a group plan
*
* @return undefined
*/
api.cancelGroupUsersSubscription = async function cancelGroupUsersSubscription (group) {
let members;
if (group.type === 'guild') {
members = await User.find({guilds: group._id}).select('_id guilds purchased').exec();
} else {
members = await User.find({'party._id': group._id}).select('_id guilds purchased').exec();
}
let promises = members.map((member) => {
return this.cancelGroupSubscriptionForUser(member, group);
});
await Promise.all(promises);
};
api.cancelGroupSubscriptionForUser = async function cancelGroupSubscriptionForUser (user, group) {
if (user.purchased.plan.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) return;
let userGroups = _.clone(user.guilds.toObject());
userGroups.push('party');
let index = userGroups.indexOf(group._id);
userGroups.splice(index, 1);
let groupPlansQuery = {
type: {$in: ['guild', 'party']},
// privacy: 'private',
_id: {$in: userGroups},
'purchased.plan.dateTerminated': null,
};
let groupFields = `${basicGroupFields} purchased`;
let userGroupPlans = await Group.find(groupPlansQuery).select(groupFields).exec();
if (userGroupPlans.length === 0) {
let leader = await User.findById(group.leader).exec();
txnEmail(user, 'group-member-cancel', [
{name: 'LEADER', content: leader.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
await this.cancelSubscription({user});
}
};
api.createSubscription = async function createSubscription (data) {
let recipient = data.gift ? data.gift.member : data.user;
let block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
@@ -75,6 +229,8 @@ api.createSubscription = async function createSubscription (data) {
emailType = 'group-subscription-begins';
groupId = group._id;
recipient.purchased.plan.quantity = data.sub.quantity;
await this.addSubscriptionToGroupUsers(group);
}
plan = recipient.purchased.plan;
@@ -135,7 +291,8 @@ api.createSubscription = async function createSubscription (data) {
revealMysteryItems(recipient);
}
if (!data.gift) {
// @TODO: Create a factory pattern for use cases
if (!data.gift && data.customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) {
txnEmail(data.user, emailType);
}
@@ -225,22 +382,6 @@ api.createSubscription = async function createSubscription (data) {
});
};
api.updateStripeGroupPlan = async function updateStripeGroupPlan (group, stripeInc) {
if (group.purchased.plan.paymentMethod !== 'Stripe') return;
let stripeApi = stripeInc || stripe;
let plan = shared.content.subscriptionBlocks.group_monthly;
await stripeApi.subscriptions.update(
group.purchased.plan.subscriptionId,
{
plan: plan.key,
quantity: group.memberCount + plan.quantity - 1,
}
);
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
};
// Sets their subscription to be cancelled later
api.cancelSubscription = async function cancelSubscription (data) {
let plan;
@@ -248,6 +389,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
let cancelType = 'unsubscribe';
let groupId;
let emailType = 'cancel-subscription';
let emailMergeData = [];
// If we are buying a group subscription
if (data.groupId) {
@@ -265,12 +407,23 @@ api.cancelSubscription = async function cancelSubscription (data) {
}
plan = group.purchased.plan;
emailType = 'group-cancel-subscription';
emailMergeData.push({name: 'GROUP_NAME', content: group.name});
await this.cancelGroupUsersSubscription(group);
} else {
plan = data.user.purchased.plan;
}
let customerId = plan.customerId;
let now = moment();
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : 30;
let defaultRemainingDays = 30;
if (plan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID) {
defaultRemainingDays = 2;
}
let remaining = data.nextBill ? moment(data.nextBill).diff(new Date(), 'days') : defaultRemainingDays;
if (plan.extraMonths < 0) plan.extraMonths = 0;
let extraDays = Math.ceil(30.5 * plan.extraMonths);
let nowStr = `${now.format('MM')}/${moment(plan.dateUpdated).format('DD')}/${now.format('YYYY')}`;
let nowStrFormat = 'MM/DD/YYYY';
@@ -289,7 +442,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
await data.user.save();
}
txnEmail(data.user, emailType);
if (customerId !== this.constants.GROUP_PLAN_CUSTOMER_ID) txnEmail(data.user, emailType, emailMergeData);
if (group) {
cancelType = 'group-unsubscribe';
+23 -7
View File
@@ -42,6 +42,22 @@ paypal.configure({
let api = {};
api.constants = {
// CURRENCY_CODE: 'USD',
// SELLER_NOTE: 'Habitica Payment',
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
// STORE_NAME: 'Habitica',
//
// GIFT_TYPE_GEMS: 'gems',
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
//
// METHOD_BUY_GEMS: 'buyGems',
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD: 'Paypal',
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};
api.paypalPaymentCreate = Bluebird.promisify(paypal.payment.create, {context: paypal.payment});
api.paypalPaymentExecute = Bluebird.promisify(paypal.payment.execute, {context: paypal.payment});
api.paypalBillingAgreementCreate = Bluebird.promisify(paypal.billingAgreement.create, {context: paypal.billingAgreement});
@@ -68,7 +84,7 @@ api.checkout = async function checkout (options = {}) {
let createPayment = {
intent: 'sale',
payer: { payment_method: 'Paypal' },
payer: { payment_method: this.constants.PAYMENT_METHOD },
redirect_urls: {
return_url: `${BASE_URL}/paypal/checkout/success`,
cancel_url: `${BASE_URL}`,
@@ -103,7 +119,7 @@ api.checkoutSuccess = async function checkoutSuccess (options = {}) {
let data = {
user,
customerId,
paymentMethod: 'Paypal',
paymentMethod: this.constants.PAYMENT_METHOD,
};
if (gift) {
@@ -138,7 +154,7 @@ api.subscribe = async function subscribe (options = {}) {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
payment_method: this.constants.PAYMENT_METHOD,
},
};
let billingAgreement = await this.paypalBillingAgreementCreate(billingAgreementAttributes);
@@ -154,7 +170,7 @@ api.subscribeSuccess = async function subscribeSuccess (options = {}) {
user,
groupId,
customerId: result.id,
paymentMethod: 'Paypal',
paymentMethod: this.constants.PAYMENT_METHOD,
sub: block,
headers,
});
@@ -194,7 +210,7 @@ api.subscribeCancel = async function subscribeCancel (options = {}) {
await payments.cancelSubscription({
user,
groupId,
paymentMethod: 'Paypal',
paymentMethod: this.constants.PAYMENT_METHOD,
nextBill: nextBillingDate,
});
};
@@ -208,7 +224,7 @@ api.ipn = async function ipnApi (options = {}) {
// @TODO: Should this request billing date?
let user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec();
if (user) {
await payments.cancelSubscription({ user, paymentMethod: 'Paypal' });
await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD });
return;
}
@@ -219,7 +235,7 @@ api.ipn = async function ipnApi (options = {}) {
.exec();
if (group) {
await payments.cancelSubscription({ groupId: group._id, paymentMethod: 'Paypal' });
await payments.cancelSubscription({ groupId: group._id, paymentMethod: this.constants.PAYMENT_METHOD });
}
};
+44 -5
View File
@@ -16,11 +16,32 @@ import {
} from '../models/group';
import shared from '../../common';
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
const i18n = shared.i18n;
let api = {};
api.constants = {
// CURRENCY_CODE: 'USD',
// SELLER_NOTE: 'Habitica Payment',
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
// STORE_NAME: 'Habitica',
//
// GIFT_TYPE_GEMS: 'gems',
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
//
// METHOD_BUY_GEMS: 'buyGems',
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
PAYMENT_METHOD: 'Stripe',
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
};
api.setStripeApi = function setStripeApi (stripeInc) {
stripe = stripeInc;
};
/**
* Allows for purchasing a user subscription, group subscription or gems with Stripe
*
@@ -97,7 +118,7 @@ api.checkout = async function checkout (options, stripeInc) {
await payments.createSubscription({
user,
customerId: response.id,
paymentMethod: 'Stripe',
paymentMethod: this.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
@@ -108,7 +129,7 @@ api.checkout = async function checkout (options, stripeInc) {
let data = {
user,
customerId: response.id,
paymentMethod: 'Stripe',
paymentMethod: this.constants.PAYMENT_METHOD,
gift,
};
@@ -204,20 +225,38 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription'));
// @TODO: Handle error response
let customer = await stripeApi.customers.retrieve(customerId);
let subscription = customer.subscription;
if (!subscription) {
if (!subscription && customer.subscriptions) {
subscription = customer.subscriptions.data[0];
}
if (!subscription) return;
await stripeApi.customers.del(customerId);
await payments.cancelSubscription({
user,
groupId,
nextBill: subscription.current_period_end * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
paymentMethod: this.constants.PAYMENT_METHOD,
});
};
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
let stripeApi = stripe;
let plan = shared.content.subscriptionBlocks.group_monthly;
await stripeApi.subscriptions.update(
group.purchased.plan.subscriptionId,
{
plan: plan.key,
quantity: group.memberCount + plan.quantity - 1,
}
);
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
};
module.exports = api;
+36 -1
View File
@@ -10,6 +10,7 @@ import { model as Challenge} from './challenge';
import * as Tasks from './task';
import validator from 'validator';
import { removeFromArray } from '../libs/collectionManipulators';
import payments from '../libs/payments';
import { groupChatReceivedWebhook } from '../libs/webhook';
import {
InternalServerError,
@@ -28,6 +29,8 @@ import {
import {
schema as SubscriptionPlanSchema,
} from './subscriptionPlan';
import amazonPayments from '../libs/amazonPayments';
import stripePayments from '../libs/stripePayments';
const questScrolls = shared.content.quests;
const Schema = mongoose.Schema;
@@ -904,10 +907,14 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
let group = this;
let update = {};
if (group.memberCount <= 1 && group.privacy === 'private' && group.isSubscribed()) {
if (group.memberCount <= 1 && group.privacy === 'private' && group.hasNotCancelled()) {
throw new NotAuthorized(shared.i18n.t('cannotDeleteActiveGroup'));
}
if (group.leader === user._id && group.hasNotCancelled()) {
throw new NotAuthorized(shared.i18n.t('leaderCannotLeaveGroupWithActiveGroup'));
}
// only remove user from challenges if it's set to leave-challenges
if (keepChallenges === 'leave-challenges') {
let challenges = await Challenge.find({
@@ -948,6 +955,10 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
update.$unset = {[`quest.members.${user._id}`]: 1};
}
if (group.purchased.plan.customerId) {
promises.push(payments.cancelGroupSubscriptionForUser(user, this));
}
// If user is the last one in group and group is private, delete it
if (group.memberCount <= 1 && group.privacy === 'private') {
// double check the member count is correct so we don't accidentally delete a group that still has users in it
@@ -957,7 +968,9 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
} else {
members = await User.find({'party._id': group._id}).select('_id').exec();
}
_.remove(members, {_id: user._id});
if (members.length === 0) {
promises.push(group.remove());
return await Bluebird.all(promises);
@@ -1165,6 +1178,28 @@ schema.methods.isSubscribed = function isSubscribed () {
return plan && plan.customerId && (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now));
};
schema.methods.hasNotCancelled = function hasNotCancelled () {
let plan = this.purchased.plan;
return this.isSubscribed() && !plan.dateTerminated;
};
schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) {
// Recheck the group plan count
let members;
if (this.type === 'guild') {
members = await User.find({guilds: this._id}).select('_id').exec();
} else {
members = await User.find({'party._id': this._id}).select('_id').exec();
}
this.memberCount = members.length;
if (this.purchased.plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
await stripePayments.chargeForAdditionalGroupMember(this);
} else if (this.purchased.plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD && !removingMember) {
await amazonPayments.chargeForAdditionalGroupMember(this);
}
};
export let model = mongoose.model('Group', schema);
// initialize tavern if !exists (fresh installs)
+33 -1
View File
@@ -1,3 +1,4 @@
import moment from 'moment';
import common from '../../../common';
import Bluebird from 'bluebird';
import {
@@ -7,9 +8,21 @@ import {
import { defaults } from 'lodash';
import { model as UserNotification } from '../userNotification';
import schema from './schema';
import payments from '../../libs/payments';
import amazonPayments from '../../libs/amazonPayments';
import stripePayments from '../../libs/stripePayments';
import paypalPayments from '../../libs/paypalPayments';
schema.methods.isSubscribed = function isSubscribed () {
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
let now = new Date();
let plan = this.purchased.plan;
return plan && plan.customerId && (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now));
};
schema.methods.hasNotCancelled = function hasNotCancelled () {
let plan = this.purchased.plan;
return this.isSubscribed() && !plan.dateTerminated;
};
// Get an array of groups ids the user is member of
@@ -91,3 +104,22 @@ schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb
return statsObject;
};
// @TODO: There is currently a three way relation between the user, payment methods and the payment helper
// This creates some odd Dependency Injection issues. To counter that, we use the user as the third layer
// To negotiate between the payment providers and the payment helper (which probably has too many responsiblities)
// In summary, currently is is best practice to use this method to cancel a user subscription, rather than calling the
// payment helper.
schema.methods.cancelSubscription = async function cancelSubscription () {
let plan = this.purchased.plan;
if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) {
return await amazonPayments.cancelSubscription({user: this});
} else if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
return await stripePayments.cancelSubscription({user: this});
} else if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) {
return await paypalPayments.subscribeCancel({user: this});
}
return await payments.cancelSubscription({user: this});
};
@@ -47,17 +47,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template',
div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.customerId !== "group-plan"', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
.container-fluid.slight-vertical-padding(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
small.muted=env.t('subscribeUsing')
.row.text-center
.col-xs-4
.col-md-4
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
.col-xs-4
.col-md-4
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
.col-xs-4
.col-md-4
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
+1 -1
View File
@@ -17,7 +17,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
li(ng-show='group.purchased.active && group.leader._id === user._id')
a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
li
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('subscription')
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('paymentDetails')
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && !group.purchased.plan.customerId')=env.t('upgradeTitle')
.tab-content
@@ -37,8 +37,8 @@ mixin groupSubscription()
.row.text-center
h3 Upgrade My Group
div
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})', ng-disabled='!_subscription.key')= env.t('card')
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})")
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id, group: group})', ng-disabled='!_subscription.key')= env.t('card')
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id, group: group})")
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
//- .col-xs-4
//- a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
@@ -27,36 +27,35 @@ div(ng-if='(task.type !== "reward") || (!obj.auth && obj.purchased && obj.purcha
hr
.form-group
legend.option-title=env.t('repeat')
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
option(value='daily')=env.t('daily')
option(value='weekly')=env.t('weekly')
option(value='monthly')=env.t('monthly')
option(value='yearly')=env.t('yearly')
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
//- option(value='weekly')=env.t('repeatWeek')
//- option(value='daily')=env.t('repeatDays')
fieldset.option-group.advanced-option(ng-show="task._edit._advanced && task.type !== 'reward'")
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
option(value='daily')=env.t('daily')
option(value='weekly')=env.t('weekly')
option(value='monthly')=env.t('monthly')
option(value='yearly')=env.t('yearly')
include ./dailies/repeat_options
.form-group(ng-show='task._edit.frequency === "monthly"')
legend.option-title=env.t('repeatsOn')
label
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth')
=env.t('dayOfMonth')
label
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek')
=env.t('dayOfWeek')
.form-group
legend.option-title=env.t('summary')
div {{summary}}
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
//- option(value='weekly')=env.t('repeatWeek')
//- option(value='daily')=env.t('repeatDays')
hr
include ./dailies/repeat_options
fieldset.option-group.advanced-option(ng-show="task._edit._advanced")
.form-group(ng-show='task._edit.frequency === "monthly"')
legend.option-title=env.t('repeatsOn')
label
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth')
=env.t('dayOfMonth')
label
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek')
=env.t('dayOfWeek')
.form-group
legend.option-title=env.t('summary')
div {{summary}}
hr
fieldset.option-group.advanced-option(ng-show="task._edit._advanced && task.type !== 'reward'")
legend.option-title
a.hint.priority-multiplier-help(href='http://habitica.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty')
ul.priority-multiplier
@@ -1,20 +1,14 @@
//- .form-group
//- legend.option-title=env.t('repeat')
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
//- option(value='weekly')=env.t('repeatWeek')
//- option(value='daily')=env.t('repeatDays')
.form-group
legend.option-title
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
legend.option-title
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
// If frequency is daily
ng-form.form-group(name='everyX')
.input-group
input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required)
span.input-group-addon {{repeatSuffix}}
// If frequency is daily
ng-form.form-group(name='everyX')
.input-group
input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required)
span.input-group-addon {{repeatSuffix}}
// If frequency is weekly
.form-group(ng-if='task._edit.frequency=="weekly"')
legend.option-title=env.t('onDays')
ul.repeat-days