Thehollidayinn/group plans part 2 (#8262)

* Added all ui components back

* Added group ui items back and initial group approval directive

* Added approval list view with approving functionality

* Added notification display for group approvals

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added per use group plan for stripe

* Added tests for stripe group plan upgrade

* Removed paypal option

* Abstract sub blocks. Hit group sub block from user settings page. Added group subscriptin beneifts display

* Fixed lint issue

* Added pricing and adjusted styles

* Moved text to translations

* Added group email types

* Fixed typo

* Fixed group plan abstraction and other style issues

* Fixed email unit test

* Added type to group plan to filter our group plans

* Removed dev protection from routes

* Removed hard coding and fixed upgrade plan

* Added error when group has subscription and tries to remove

* Fixed payment unit tests

* Added custom string and moved subscription check up in the logic

* Added ability for old leader to delete subscription the created

* Allowed old guild leader to edit their group subscription

* Fixed linting and tests

* Added group sub page to user sub settings

* Added approval and group tasks requests back. Hid user group sub on profile

* Added group tasks sync after adding to allow for editing

* Fixed promise chain when resolving group

* Added approvals to group promise chain

* Ensured compelted group todos are not delted at cron

* Updated copy and other minor styles

* Added group field to tags and recolored group tag.

* Added chat message when task is claimed

* Preventing task scoring when approval is needed

* Added approval requested indicator

* Updated column with for tasks on group page

* Added checklist sync on assign

* Added sync for checklist items

* Added checkilist sync when task is updated

* Added checklist sync remove

* Sanatized group tasks when updated

* Fixed lint issues

* Added instant scoring of approved task

* Added task modal

* Fixed editing of challenge and group tasks

* Added cancel button

* Added add new checklist option to update sync

* Added remove for checklist

* Added checklist update

* Added difference check and sync for checklist if there is a diff

* Fixed task syncing

* Fixed linting issues

* Fixed styles and karma tests

* Fixed minor style issues

* Fixed obj transfer on scope

* Fixed broken tests

* Added new benefits page

* Updated group page styles

* Updated benefits page style

* Added translations

* Prevented sync with empty trask list

* Added task title to edit modal

* Added new group plans page and upgrade redirect

* Added group plans redirect to upgrade

* Fixed party home page being hidden and home button click

* Fixed dynamic changing of task status and grey popup

* Fixed tag editing

* Hid benifites information if group has subscription

* Added quotes to task name

* Fixed issue with assigning multiple users

* Added new group plans ctrl

* Hid menu from public guilds

* Fixed task sync issue

* Updated placeholder for assign field

* Added correct cost to subscribe details

* Hid create, edit, delete task options from non group leaders

* Prevented some front end modifications to group tasks

* Hid tags option from group original task

* Added refresh for approvals and group tasks

* Prepend new group tasks

* Fix last checklist item sync

* Fixed casing issue with tags

* Added claimed by message on hover

* Prevent user from deleting assigned task

* Added single route for group plan sign up and payments

* Abstracted stripe payments and added initial tests

* Abstracted amazon and added initial tests

* Fixed create group message

* Update group id check and return group

* Updated to use the new returned group

* Fixed linting and promise issues

* Fixed broken leave test after merge issue

* Fixed undefined approval error and editing/deleting challenge tasks

* Add pricing to group plans, removed confirmation, and fixed redirect after payment

* Updated group plan cost text
This commit is contained in:
Keith Holliday
2016-12-21 13:45:45 -06:00
committed by GitHub
parent 55a8eef3e1
commit ea24eeb019
70 changed files with 2002 additions and 736 deletions

View File

@@ -207,6 +207,7 @@
"selenium-server": "2.53.0", "selenium-server": "2.53.0",
"sinon": "^1.17.2", "sinon": "^1.17.2",
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0",
"superagent-defaults": "^0.1.13", "superagent-defaults": "^0.1.13",
"vinyl-transform": "^1.0.0", "vinyl-transform": "^1.0.0",
"webpack-dev-middleware": "^1.4.0", "webpack-dev-middleware": "^1.4.0",

View File

@@ -69,4 +69,16 @@ describe('DELETE /tasks/:id', () => {
expect(syncedTask.group.broken).to.equal('TASK_DELETED'); expect(syncedTask.group.broken).to.equal('TASK_DELETED');
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED'); expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
}); });
it('prevents a user from deleting a task they are assigned to', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.del(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDeleteAssignedGroupTasks'),
});
});
}); });

View File

@@ -59,9 +59,11 @@ describe('POST /tasks/:id/approve/:userId', () => {
await member.sync(); await member.sync();
expect(member.notifications.length).to.equal(1); expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved')); expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(syncedTask.group.approval.approved).to.be.true; expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id); expect(syncedTask.group.approval.approvingUser).to.equal(user._id);

View File

@@ -82,7 +82,7 @@ describe('POST /tasks/:taskId', () => {
}); });
}); });
it('allows user to assign themselves', async () => { it('allows user to assign themselves (claim)', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`); await member.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await user.get(`/tasks/group/${guild._id}`); let groupTask = await user.get(`/tasks/group/${guild._id}`);
@@ -93,6 +93,14 @@ describe('POST /tasks/:taskId', () => {
expect(syncedTask).to.exist; expect(syncedTask).to.exist;
}); });
it('sends a message to the group when a user claims a task', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
let updateGroup = await user.get(`/groups/${guild._id}`);
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
});
it('assigns a task to a user', async () => { it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);

View File

@@ -4,15 +4,20 @@ import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications'; import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group'; import { model as Group } from '../../../../../website/server/models/group';
import stripeModule from 'stripe';
import moment from 'moment'; import moment from 'moment';
import { translate as t } from '../../../../helpers/api-v3-integration.helper'; import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import { import {
generateGroup, generateGroup,
} from '../../../../helpers/api-unit.helper.js'; } from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
import amzLib from '../../../../../website/server/libs/amazonPayments';
describe('payments/index', () => { describe('payments/index', () => {
let user, group, data, plan; let user, group, data, plan;
let stripe = stripeModule('test');
beforeEach(async () => { beforeEach(async () => {
user = new User(); user = new User();
user.profile.name = 'sender'; user.profile.name = 'sender';
@@ -625,7 +630,40 @@ describe('payments/index', () => {
await api.cancelSubscription(data); await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce; expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription'); expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
}); });
}); });
}); });
@@ -732,4 +770,156 @@ describe('payments/index', () => {
}); });
}); });
}); });
describe('#upgradeGroupPlan', () => {
let spy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.false;
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
describe('payWithStripe', () => {
let spy;
let stripeCreateCustomerSpy;
let createSubSpy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
subscriptions: {
data: [{id: 'test-id'}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let token = 'test-token';
let gift;
let sub = data.sub;
let groupId = group._id;
let email = 'test@test.com';
let headers = {};
let coupon;
await api.payWithStripe([
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
], stripe);
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
describe('subscribeWithAmazon', () => {
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(function () {
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let billingAgreementId = 'billingAgreementId';
let sub = data.sub;
let coupon;
let groupId = group._id;
let headers = {};
await api.subscribeWithAmazon([
billingAgreementId,
sub,
coupon,
sub,
user,
groupId,
headers,
]);
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
}); });

View File

@@ -1,13 +1,16 @@
import { sleep } from '../../../../helpers/api-unit.helper'; import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group'; import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import { BadRequest } from '../../../../../website/server/libs/errors'; import {
BadRequest,
} from '../../../../../website/server/libs/errors';
import { quests as questScrolls } from '../../../../../website/common/script/content'; import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook'; import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email'; import * as email from '../../../../../website/server/libs/email';
import validator from 'validator'; import validator from 'validator';
import { TAVERN_ID } from '../../../../../website/common/script/'; import { TAVERN_ID } from '../../../../../website/common/script/';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import shared from '../../../../../website/common';
describe('Group Model', () => { describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember; let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
@@ -638,6 +641,22 @@ describe('Group Model', () => {
expect(party).to.not.exist; expect(party).to.not.exist;
}); });
it('does not delete a private group when the last member leaves and a subscription is active', async () => {
party.memberCount = 1;
party.purchased.plan.customerId = '110002222333';
await expect(party.leave(participatingMember))
.to.eventually.be.rejected.and.to.eql({
name: 'NotAuthorized',
httpCode: 401,
message: shared.i18n.t('cannotDeleteActiveGroup'),
});
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
expect(party.memberCount).to.eql(1);
});
it('does not delete a public group when the last member leaves', async () => { it('does not delete a public group when the last member leaves', async () => {
party.privacy = 'public'; party.privacy = 'public';

View File

@@ -2,7 +2,7 @@ import { model as Challenge } from '../../../../../website/server/models/challen
import { model as Group } from '../../../../../website/server/models/group'; import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task'; import * as Tasks from '../../../../../website/server/models/task';
import { each, find } from 'lodash'; import { each, find, findIndex } from 'lodash';
describe('Group Task Methods', () => { describe('Group Task Methods', () => {
let guild, leader, challenge, task; let guild, leader, challenge, task;
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue)); task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
task.group.id = guild._id; task.group.id = guild._id;
await task.save(); await task.save();
if (task.checklist) {
task.checklist.push({
text: 'Checklist Item 1',
completed: false,
});
}
}); });
it('syncs an assigned task to a user', async () => { it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, leader); await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id});
let tagIndex = findIndex(updatedLeader.tags, {id: guild._id});
let newTag = updatedLeader.tags[tagIndex];
expect(newTag.id).to.equal(guild._id);
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id}); let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}}); let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask); let syncedTask = find(updatedLeadersTasks, findLinkedTask);
@@ -96,38 +114,124 @@ describe('Group Task Methods', () => {
expect(syncedTask.text).to.equal(task.text); expect(syncedTask.text).to.equal(task.text);
}); });
it('syncs updated info for assigned task to all users', async () => { it('syncs checklist items to an assigned user', async () => {
let newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader); await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id}); let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}}); let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask); let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id}); if (task.type !== 'daily' && task.type !== 'todo') return;
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id); expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask).to.exist; expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
expect(syncedTask.text).to.equal(task.text); });
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id); describe('syncs updated info', async() => {
expect(syncedMemberTask).to.exist; let newMember;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true); beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, {newCheckListItem});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, {updateCheckListItems: [task.checklist[0]]});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, {removedCheckListItemId: task.checklist[0].id});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
}); });
it('removes an assigned task and unlinks assignees', async () => { it('removes an assigned task and unlinks assignees', async () => {

View File

@@ -275,7 +275,8 @@ describe('Challenges Controller', function() {
describe('editTask', function() { describe('editTask', function() {
it('is Tasks.editTask', function() { it('is Tasks.editTask', function() {
inject(function(Tasks) { inject(function(Tasks) {
expect(scope.editTask).to.eql(Tasks.editTask); // @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
// expect(scope.editTask).to.eql(Tasks.editTask);
}); });
}); });
}); });

View File

@@ -32,7 +32,8 @@ describe('Tasks Controller', function() {
describe('editTask', function() { describe('editTask', function() {
it('is Tasks.editTask', function() { it('is Tasks.editTask', function() {
inject(function(Tasks) { inject(function(Tasks) {
expect(scope.editTask).to.eql(Tasks.editTask); // @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
// expect(scope.editTask).to.eql(Tasks.editTask);
}); });
}); });
}); });

View File

@@ -16,6 +16,8 @@ describe('Tasks Service', function() {
rootScope.charts = {}; rootScope.charts = {};
tasks = Tasks; tasks = Tasks;
}); });
rootScope.openModal = function () {};
}); });
it('calls get user tasks endpoint', function() { it('calls get user tasks endpoint', function() {
@@ -151,7 +153,6 @@ describe('Tasks Service', function() {
}); });
describe('editTask', function() { describe('editTask', function() {
var task; var task;
beforeEach(function(){ beforeEach(function(){

View File

@@ -157,6 +157,16 @@ describe('shared.ops.scoreTask', () => {
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta); expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
}); });
it('does not modify stats when task need approval', () => {
todo.group.approval.required = true;
options = { user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false };
scoreTask(options);
expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
});
context('habits', () => { context('habits', () => {
it('up', () => { it('up', () => {
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false }; options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };

View File

@@ -13,5 +13,7 @@ chai.use(require('sinon-chai'));
chai.use(require('chai-as-promised')); chai.use(require('chai-as-promised'));
global.expect = chai.expect; global.expect = chai.expect;
global.sinon = require('sinon'); global.sinon = require('sinon');
let sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(global.sinon);
global.sandbox = sinon.sandbox.create(); global.sandbox = sinon.sandbox.create();
global.Promise = Bluebird; global.Promise = Bluebird;

View File

@@ -8,59 +8,67 @@
// array of keywords and their associated color vars // array of keywords and their associated color vars
$stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $good) (better $better) (best $best) $stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $good) (better $better) (best $best)
taskContainerStyles($stage)
background-color: $stage[1]
border: 1px solid shade($stage[1],10%)
.priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency
li
hrpg-button-color-mixin($stage[1])
button
&.active
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.plusminus
.task-checker
label:after
border: 1px solid darken($stage[1], 30%) !important
input[type=checkbox]:checked + label:after
//border: 1px solid darken($stage[1], 50%) !important
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
.save-close, .task-checklist-edit li
hrpg-button-color-mixin(darken($stage[1],5%))
button
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.task-actions
background-color: darken($stage[1], 30%)
.action-yesno label,
.task-action-btn,
.task-actions a
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
input[type=checkbox].task-input:focus + label, input.habit:focus + a
background-color: darken($stage[1], 40%)
.task-actions a:nth-of-type(2)
border-top: 1px solid darken($stage[1],50%) // If there are two habit buttons (+ -), add a border to separate them
.task-options
background-color: $stage[1]
.option-group:not(.task-checklist)
border-bottom: 1px solid darken($stage[1], 15%)
.option-content
border-color: darken($stage[1], 16.18%) !important
&:hover
border-color: darken($stage[1], 32.8%) !important
&:focus
border-color: darken($stage[1], 61.8%) !important
outline: none;
// for each color stage, generate a named class w/ the appropriate color // for each color stage, generate a named class w/ the appropriate color
for $stage in $stages for $stage in $stages
.task-column:not(.rewards) .task-column:not(.rewards)
.color-{$stage[0]}:not(.completed) .color-{$stage[0]}:not(.completed)
background-color: $stage[1] taskContainerStyles($stage)
border: 1px solid shade($stage[1],10%)
.priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency .task-modal
li &.color-{$stage[0]}:not(.completed)
hrpg-button-color-mixin($stage[1]) taskContainerStyles($stage)
button
&.active
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.plusminus
.task-checker
label:after
border: 1px solid darken($stage[1], 30%) !important
input[type=checkbox]:checked + label:after
//border: 1px solid darken($stage[1], 50%) !important
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
.save-close, .task-checklist-edit li
hrpg-button-color-mixin(darken($stage[1],5%))
button
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.task-actions
background-color: darken($stage[1], 30%)
.action-yesno label,
.task-action-btn,
.task-actions a
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
input[type=checkbox].task-input:focus + label, input.habit:focus + a
background-color: darken($stage[1], 40%)
.task-actions a:nth-of-type(2)
border-top: 1px solid darken($stage[1],50%) // If there are two habit buttons (+ -), add a border to separate them
.task-options
background-color: $stage[1]
.option-group:not(.task-checklist)
border-bottom: 1px solid darken($stage[1], 15%)
.option-content
border-color: darken($stage[1], 16.18%) !important
&:hover
border-color: darken($stage[1], 32.8%) !important
&:focus
border-color: darken($stage[1], 61.8%) !important
outline: none;
// completed has to be outside the loop to override the color class // completed has to be outside the loop to override the color class
.completed .completed
@@ -366,6 +374,12 @@ for $stage in $stages
text-align: center text-align: center
opacity: 0.75 opacity: 0.75
// Group yesno
.group-yesno
label:hover:after, input[type=checkbox]:checked + label:after
content: "" !important
opacity: 1 !important
// secondary task commands // secondary task commands
// ----------------------- // -----------------------

View File

@@ -139,6 +139,13 @@ window.habitrpg = angular.module('habitrpg',
title: env.t('titlePatrons') title: env.t('titlePatrons')
}) })
.state('options.social.groupPlans', {
url: '/group-plans',
templateUrl: "partials/options.social.groupPlans.html",
controller: 'GroupPlansCtrl',
title: env.t('groupPlansTitle')
})
.state('options.social.guilds', { .state('options.social.guilds', {
url: '/guilds', url: '/guilds',
templateUrl: "partials/options.social.guilds.html", templateUrl: "partials/options.social.guilds.html",
@@ -155,38 +162,55 @@ window.habitrpg = angular.module('habitrpg',
templateUrl: "partials/options.social.guilds.create.html", templateUrl: "partials/options.social.guilds.create.html",
title: env.t('titleGuilds') title: env.t('titleGuilds')
}) })
.state('options.social.guilds.detail', { .state('options.social.guilds.detail', {
url: '/:gid', url: '/:gid',
templateUrl: 'partials/options.social.guilds.detail.html', templateUrl: 'partials/options.social.guilds.detail.html',
title: env.t('titleGuilds'), title: env.t('titleGuilds'),
controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', 'User', '$location',
function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks) { function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks, User, $location) {
$scope.groupPanel = 'chat';
$scope.upgrade = false;
// @TODO: Move this to service or single http request
Groups.Group.get($stateParams.gid) Groups.Group.get($stateParams.gid)
.then(function (response) { .then(function (response) {
$scope.obj = $scope.group = response.data.data; $scope.obj = $scope.group = response.data.data;
Chat.markChatSeen($scope.group._id); return Chat.markChatSeen($scope.group._id);
Members.getGroupMembers($scope.group._id)
.then(function (response) {
$scope.group.members = response.data.data;
});
Members.getGroupInvites($scope.group._id)
.then(function (response) {
$scope.group.invites = response.data.data;
});
Challenges.getGroupChallenges($scope.group._id)
.then(function (response) {
$scope.group.challenges = response.data.data;
});
//@TODO: Add this back when group tasks go live
// Tasks.getGroupTasks($scope.group._id);
// .then(function (response) {
// var tasks = response.data.data;
// tasks.forEach(function (element, index, array) {
// if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
// $scope.group[element.type + 's'].push(element);
// })
// });
}) })
.then (function () {
return Members.getGroupMembers($scope.group._id);
})
.then(function (response) {
$scope.group.members = response.data.data;
return Members.getGroupInvites($scope.group._id);
})
.then(function (response) {
$scope.group.invites = response.data.data;
return Challenges.getGroupChallenges($scope.group._id);
})
.then(function (response) {
$scope.group.challenges = response.data.data;
return Tasks.getGroupTasks($scope.group._id);
})
.then(function (response) {
var tasks = response.data.data;
tasks.forEach(function (element, index, array) {
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
$scope.group[element.type + 's'].unshift(element);
});
$scope.group.approvals = [];
if (User.user._id === $scope.group.leader._id) {
return Tasks.getGroupApprovals($scope.group._id);
}
})
.then(function (response) {
if (response) $scope.group.approvals = response.data.data;
});
}] }]
}) })

View File

@@ -1,21 +1,23 @@
habitrpg.controller('GroupApprovalsCtrl', ['$scope', 'Tasks', habitrpg.controller('GroupApprovalsCtrl', ['$scope', 'Tasks',
function ($scope, Tasks) { function ($scope, Tasks) {
$scope.approvals = []; $scope.approve = function (taskId, userId, username, $index) {
if (!confirm(env.t('confirmTaskApproval', {username: username}))) return;
// Tasks.getGroupApprovals($scope.group._id)
// .then(function (response) {
// $scope.approvals = response.data.data;
// });
$scope.approve = function (taskId, userId, $index) {
if (!confirm(env.t('confirmTaskApproval'))) return;
Tasks.approve(taskId, userId) Tasks.approve(taskId, userId)
.then(function (response) { .then(function (response) {
$scope.approvals.splice($index, 1); $scope.group.approvals.splice($index, 1);
}); });
}; };
$scope.approvalTitle = function (approval) { $scope.approvalTitle = function (approval) {
return env.t('approvalTitle', {text: approval.text, userName: approval.userId.profile.name}); return env.t('approvalTitle', {text: approval.text, userName: approval.userId.profile.name});
}; };
$scope.refreshApprovals = function () {
$scope.loading = true;
Tasks.getGroupApprovals($scope.group._id)
.then(function (response) {
if (response) $scope.group.approvals = response.data.data;
$scope.loading = false;
});
};
}]); }]);

View File

@@ -27,12 +27,17 @@
})); }));
var currentTags = []; var currentTags = [];
_.each(scope.task.group.assignedUsers, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) }) _.each(scope.task.group.assignedUsers, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) });
var allowedTags = [];
_.each(scope.task.group.members, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) });
var taggle = new Taggle('taggle', { var taggle = new Taggle('taggle', {
tags: currentTags, tags: currentTags,
allowedTags: currentTags, allowedTags: allowedTags,
allowDuplicates: false, allowDuplicates: false,
preserveCase: true,
placeholder: window.env.t('assignFieldPlaceholder'),
onBeforeTagAdd: function(event, tag) { onBeforeTagAdd: function(event, tag) {
return confirm(window.env.t('confirmAddTag', {tag: tag})); return confirm(window.env.t('confirmAddTag', {tag: tag}));
}, },

View File

@@ -2,7 +2,7 @@ habitrpg.controller('GroupTaskActionsCtrl', ['$scope', 'Shared', 'Tasks', 'User'
function ($scope, Shared, Tasks, User) { function ($scope, Shared, Tasks, User) {
$scope.assignedMembers = []; $scope.assignedMembers = [];
$scope.user = User.user; $scope.user = User.user;
$scope.task._edit.requiresApproval = false; $scope.task._edit.requiresApproval = false;
if ($scope.task.group.approval.required) { if ($scope.task.group.approval.required) {
$scope.task._edit.requiresApproval = $scope.task.group.approval.required; $scope.task._edit.requiresApproval = $scope.task.group.approval.required;

View File

@@ -1,17 +1,65 @@
habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) { habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) {
$scope.editTask = Tasks.editTask; function handleGetGroupTasks (response) {
var group = $scope.obj;
var tasks = response.data.data;
if (tasks.length === 0) return;
// @TODO: We need to get the task information from createGroupTasks rather than resyncing
group['habits'] = [];
group['dailys'] = [];
group['todos'] = [];
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);
})
$scope.loading = false;
};
$scope.refreshTasks = function () {
$scope.loading = true;
Tasks.getGroupTasks($scope.group._id)
.then(handleGetGroupTasks);
};
/*
* Task Edit functions
*/
$scope.toggleBulk = Tasks.toggleBulk; $scope.toggleBulk = Tasks.toggleBulk;
$scope.cancelTaskEdit = Tasks.cancelTaskEdit; $scope.cancelTaskEdit = Tasks.cancelTaskEdit;
$scope.editTask = function (task, user, taskStatus) {
Tasks.editTask(task, user, taskStatus, $scope);
};
function addTask (listDef, taskTexts) { function addTask (listDef, taskTexts) {
taskTexts.forEach(function (taskText) { taskTexts.forEach(function (taskText) {
var task = Shared.taskDefaults({text: taskText, type: listDef.type}); var task = Shared.taskDefaults({text: taskText, type: listDef.type});
//If the group has not been created, we bulk add tasks on save //If the group has not been created, we bulk add tasks on save
var group = $scope.obj; var group = $scope.obj;
if (group._id) Tasks.createGroupTasks(group._id, task); if (!group._id) return;
if (!group[task.type + 's']) group[task.type + 's'] = [];
group[task.type + 's'].unshift(task); Tasks.createGroupTasks(group._id, task)
.then(function () {
// Set up default group info on task. @TODO: Move this to Tasks.createGroupTasks
task.group = {
id: group._id,
approval: {required: false, approved: false, requested: false},
assignedUsers: [],
};
if (!group[task.type + 's']) group[task.type + 's'] = [];
group[task.type + 's'].unshift(task);
return Tasks.getGroupTasks($scope.group._id);
})
.then(handleGetGroupTasks);
}); });
}; };
@@ -28,8 +76,21 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
}; };
$scope.saveTask = function(task, stayOpen, isSaveAndClose) { $scope.saveTask = function(task, stayOpen, isSaveAndClose) {
Tasks.saveTask (task, stayOpen, isSaveAndClose); // Check if we have a lingering checklist that the enter button did not trigger on
Tasks.updateTask(task._id, task); var lastIndex = task._edit.checklist.length - 1;
var lastCheckListItem = task._edit.checklist[lastIndex];
if (lastCheckListItem && !lastCheckListItem.id && lastCheckListItem.text) {
Tasks.addChecklistItem(task._id, lastCheckListItem)
.then(function (response) {
task._edit.checklist[lastIndex] = response.data.data.checklist[lastIndex];
task.checklist[lastIndex] = response.data.data.checklist[lastIndex];
Tasks.saveTask(task, stayOpen, isSaveAndClose);
Tasks.updateTask(task._id, task);
});
} else {
Tasks.saveTask (task, stayOpen, isSaveAndClose);
Tasks.updateTask(task._id, task);
}
}; };
$scope.shouldShow = function(task, list, prefs){ $scope.shouldShow = function(task, list, prefs){
@@ -63,9 +124,23 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
*/ */
$scope.addChecklist = Tasks.addChecklist; $scope.addChecklist = Tasks.addChecklist;
$scope.addChecklistItem = Tasks.addChecklistItemToUI; $scope.addChecklistItem = function addChecklistItemToUI(task, $event, $index) {
if (task._edit.checklist[$index].justAdded) return;
task._edit.checklist[$index].justAdded = true;
if (!task._edit.checklist[$index].id) {
Tasks.addChecklistItem (task._id, task._edit.checklist[$index])
.then(function (response) {
task._edit.checklist[$index] = response.data.data.checklist[$index];
})
}
Tasks.addChecklistItemToUI(task, $event, $index);
};
$scope.removeChecklistItem = Tasks.removeChecklistItemFromUI; $scope.removeChecklistItem = function (task, $event, $index, force) {
if (!task._edit.checklist[$index].id) return;
Tasks.removeChecklistItem (task._id, task._edit.checklist[$index].id);
Tasks.removeChecklistItemFromUI(task, $event, $index, force);
};
$scope.swapChecklistItems = Tasks.swapChecklistItems; $scope.swapChecklistItems = Tasks.swapChecklistItems;
@@ -78,4 +153,34 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
//@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api //@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api
Tasks.updateTask(task._id, task); Tasks.updateTask(task._id, task);
}; };
$scope.checkGroupAccess = function (group) {
if (!group || !group.leader) return true;
if (User.user._id !== group.leader._id) return false;
return true;
};
/*
* Task Details
*/
$scope.taskPopover = function (task) {
if (task.popoverOpen) return '';
var content = task.notes;
if ($scope.group) {
var memberIdToProfileNameMap = _.object(_.map($scope.group.members, function(item) {
return [item.id, item.profile.name]
}));
var claimingUsers = [];
task.group.assignedUsers.forEach(function (userId) {
claimingUsers.push(memberIdToProfileNameMap[userId]);
})
if (claimingUsers.length > 0) content += window.env.t('claimedBy', {claimingUsers: claimingUsers.join(', ')});
}
return content;
};
}]); }]);

View File

@@ -34,7 +34,10 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
return User.user.challenges.indexOf(challenge._id) !== -1; return User.user.challenges.indexOf(challenge._id) !== -1;
} }
$scope.editTask = Tasks.editTask; $scope.editTask = function (task, user, taskStatus) {
Tasks.editTask(task, user, taskStatus, $scope);
};
$scope.cancelTaskEdit = Tasks.cancelTaskEdit; $scope.cancelTaskEdit = Tasks.cancelTaskEdit;
$scope.canEdit = function(task) { $scope.canEdit = function(task) {
@@ -313,6 +316,15 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
$scope.toggleBulk = Tasks.toggleBulk; $scope.toggleBulk = Tasks.toggleBulk;
/*
* Task Details
*/
$scope.taskPopover = function (task) {
if (task.popoverOpen) return '';
var content = task.notes;
return content;
};
/* /*
-------------------------- --------------------------
Subscription Subscription

View File

@@ -45,4 +45,8 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'Shared',
User.addTag({body:{name: $scope._newTag.name, id: Shared.uuid()}}); User.addTag({body:{name: $scope._newTag.name, id: Shared.uuid()}});
$scope._newTag.name = ''; $scope._newTag.name = '';
}; };
$scope.showChallengeClass = function (tag) {
return tag.challenge || tag.group;
};
}]); }]);

View File

@@ -0,0 +1,43 @@
"use strict";
/*
A controller to manage the Group Plans page
*/
angular.module('habitrpg')
.controller("GroupPlansCtrl", ['$scope', '$window', 'Groups', 'Payments',
function($scope, $window, Groups, Payments) {
$scope.PAGES = {
BENEFITS: 'benefits',
CREATE_GROUP: 'create-group',
UPGRADE_GROUP: 'upgrade-group',
};
$scope.activePage = $scope.PAGES.BENEFITS;
$scope.newGroup = {
type: 'guild',
privacy: 'private',
};
$scope.PAYMENTS = {
AMAZON: 'amazon',
STRIPE: 'stripe',
};
$scope.changePage = function (page) {
$scope.activePage = page;
$window.scrollTo(0, 0);
};
$scope.newGroupIsReady = function () {
return $scope.newGroup.name && $scope.newGroup.description;
};
$scope.createGroup = function () {
$scope.changePage($scope.PAGES.UPGRADE_GROUP);
};
$scope.upgradeGroup = function (paymentType) {
var subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
if (paymentType === $scope.PAYMENTS.STRIPE) Payments.showStripe({subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
if (paymentType === $scope.PAYMENTS.AMAZON) Payments.amazonPayments.init({type: 'subscription', subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
};
}]);

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification', habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) { function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
$scope.isMemberOfPendingQuest = function (userid, group) { $scope.isMemberOfPendingQuest = function (userid, group) {
if (!group.quest || !group.quest.members) return false; if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending if (group.quest.active) return false; // quest is started, not pending

View File

@@ -34,7 +34,7 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
Groups.Group.create(group) Groups.Group.create(group)
.then(function (response) { .then(function (response) {
var createdGroup = response.data.data; var createdGroup = response.data.data;
$rootScope.hardRedirect('/#/options/groups/guilds/' + createdGroup._id); $rootScope.hardRedirect('/#/options/groups/guilds/' + createdGroup._id + '?upgrade=true');
}); });
} }
} }

View File

@@ -11,6 +11,7 @@ angular.module('habitrpg')
function selectNotificationValue(mysteryValue, invitationValue, cardValue, unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) { function selectNotificationValue(mysteryValue, invitationValue, cardValue, unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) {
var user = $scope.user; var user = $scope.user;
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) { if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
return mysteryValue; return mysteryValue;
} else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) { } else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {

View File

@@ -1,8 +1,8 @@
'use strict'; 'use strict';
habitrpg.controller('NotificationCtrl', habitrpg.controller('NotificationCtrl',
['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social', ['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social', 'Tasks',
function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social) { function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social, Tasks) {
$rootScope.$watch('user.stats.hp', function (after, before) { $rootScope.$watch('user.stats.hp', function (after, before) {
if (after <= 0){ if (after <= 0){
@@ -87,6 +87,8 @@ habitrpg.controller('NotificationCtrl',
if (!after || after.length === 0) return; if (!after || after.length === 0) return;
var notificationsToRead = []; var notificationsToRead = [];
var scoreTaskNotification;
after.forEach(function (notification) { after.forEach(function (notification) {
if (lastShownNotifications.indexOf(notification.id) !== -1) { if (lastShownNotifications.indexOf(notification.id) !== -1) {
return; return;
@@ -141,6 +143,9 @@ habitrpg.controller('NotificationCtrl',
trasnferGroupNotification(notification); trasnferGroupNotification(notification);
markAsRead = false; markAsRead = false;
break; break;
case 'SCORED_TASK':
scoreTaskNotification = notification;
break;
case 'LOGIN_INCENTIVE': case 'LOGIN_INCENTIVE':
Notification.showLoginIncentive(User.user, notification.data, Social.loadWidgets); Notification.showLoginIncentive(User.user, notification.data, Social.loadWidgets);
break; break;
@@ -159,7 +164,17 @@ habitrpg.controller('NotificationCtrl',
if (markAsRead) notificationsToRead.push(notification.id); if (markAsRead) notificationsToRead.push(notification.id);
}); });
User.readNotifications(notificationsToRead); var userReadNotifsPromise = User.readNotifications(notificationsToRead);
if (userReadNotifsPromise) {
userReadNotifsPromise.then(function () {
if (scoreTaskNotification) {
Notification.markdown(scoreTaskNotification.data.message);
User.score({params:{task: scoreTaskNotification.data.scoreTask, direction: "up"}});
}
});
}
User.user.notifications = []; // reset the notifications User.user.notifications = []; // reset the notifications
} }

View File

@@ -10,6 +10,7 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
$scope.type = 'party'; $scope.type = 'party';
$scope.text = window.env.t('party'); $scope.text = window.env.t('party');
$scope.group = {loadingParty: true}; $scope.group = {loadingParty: true};
$scope.groupPanel = 'chat';
$scope.inviteOrStartParty = Groups.inviteOrStartParty; $scope.inviteOrStartParty = Groups.inviteOrStartParty;
$scope.loadWidgets = Social.loadWidgets; $scope.loadWidgets = Social.loadWidgets;

View File

@@ -51,11 +51,17 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
$scope.toggleBulk = Tasks.toggleBulk; $scope.toggleBulk = Tasks.toggleBulk;
$scope.editTask = Tasks.editTask; $scope.editTask = function (task, user, taskStatus) {
Tasks.editTask(task, user, taskStatus, $scope);
};
$scope.canEdit = function(task) { $scope.canEdit = function(task) {
// can't edit challenge tasks // can't edit challenge tasks
return !task.challenge.id; return !task.challenge.id && (!task.group || !task.group.id);
}
$scope.checkGroupAccess = function (group) {
return true;
} }
$scope.doubleClickTask = function (obj, task) { $scope.doubleClickTask = function (obj, task) {
@@ -225,9 +231,10 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
------------------------ ------------------------
*/ */
$scope.shouldShow = function(task, list, prefs){ $scope.shouldShow = function(task, list, prefs) {
if (task._editing) // never hide a task while being edited if (task._editing) // never hide a task while being edited
return true; return true;
var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true; var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true;
switch (list.view) { switch (list.view) {
case "yellowred": // Habits case "yellowred": // Habits
@@ -324,4 +331,13 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
return notes; return notes;
}; };
/*
* Task Details
*/
$scope.taskPopover = function (task) {
if (task.popoverOpen) return '';
var content = task.notes;
return content;
};
}]); }]);

View File

@@ -37,12 +37,23 @@ function($rootScope, User, $http, Content) {
panelLabel: sub ? window.env.t('subscribe') : window.env.t('checkout'), panelLabel: sub ? window.env.t('subscribe') : window.env.t('checkout'),
token: function(res) { token: function(res) {
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
if (data.groupToCreate) {
url = '/api/v3/groups/create-plan?a=a';
res.groupToCreate = data.groupToCreate;
res.paymentType = 'Stripe';
}
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift); if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
if (data.subscription) url += '&sub='+sub.key; if (data.subscription) url += '&sub='+sub.key;
if (data.coupon) url += '&coupon='+data.coupon; if (data.coupon) url += '&coupon='+data.coupon;
if (data.groupId) url += '&groupId=' + data.groupId; if (data.groupId) url += '&groupId=' + data.groupId;
$http.post(url, res).success(function() { $http.post(url, res).success(function(response) {
window.location.reload(true); if (response && response.data && response.data._id) {
$rootScope.hardRedirect('/#/options/groups/guilds/' + response.data._id);
} else {
window.location.reload(true);
}
}).error(function(res) { }).error(function(res) {
alert(res.message); alert(res.message);
}); });
@@ -116,6 +127,10 @@ function($rootScope, User, $http, Content) {
Payments.amazonPayments.groupId = data.groupId; Payments.amazonPayments.groupId = data.groupId;
} }
if (data.groupToCreate) {
Payments.amazonPayments.groupToCreate = data.groupToCreate;
}
Payments.amazonPayments.gift = data.gift; Payments.amazonPayments.gift = data.gift;
Payments.amazonPayments.type = data.type; Payments.amazonPayments.type = data.type;
@@ -255,14 +270,24 @@ function($rootScope, User, $http, Content) {
} else if(Payments.amazonPayments.type === 'subscription') { } else if(Payments.amazonPayments.type === 'subscription') {
var url = '/amazon/subscribe'; var url = '/amazon/subscribe';
if (Payments.amazonPayments.groupToCreate) {
url = '/api/v3/groups/create-plan';
}
$http.post(url, { $http.post(url, {
billingAgreementId: Payments.amazonPayments.billingAgreementId, billingAgreementId: Payments.amazonPayments.billingAgreementId,
subscription: Payments.amazonPayments.subscription, subscription: Payments.amazonPayments.subscription,
coupon: Payments.amazonPayments.coupon, coupon: Payments.amazonPayments.coupon,
groupId: Payments.amazonPayments.groupId, groupId: Payments.amazonPayments.groupId,
}).success(function(){ groupToCreate: Payments.amazonPayments.groupToCreate,
paymentType: 'Amazon',
}).success(function(response) {
Payments.amazonPayments.reset(); Payments.amazonPayments.reset();
window.location.reload(true); if (response && response.data && response.data._id) {
$rootScope.hardRedirect('/#/options/groups/guilds/' + response.data._id);
} else {
window.location.reload(true);
}
}).error(function(res){ }).error(function(res){
alert(res.message); alert(res.message);
Payments.amazonPayments.reset(); Payments.amazonPayments.reset();

View File

@@ -245,12 +245,31 @@ angular.module('habitrpg')
}); });
}; };
function editTask(task, user) { function editTask(task, user, taskStatus, scopeInc) {
task._editing = true; var modalScope = $rootScope.$new();
task._tags = !user.preferences.tagsCollapsed; modalScope.task = task;
task._advanced = !user.preferences.advancedCollapsed; modalScope.task._editing = true;
task._edit = angular.copy(task); modalScope.task._tags = !user.preferences.tagsCollapsed;
modalScope.task._advanced = !user.preferences.advancedCollapsed;
modalScope.task._edit = angular.copy(task);
if($rootScope.charts[task._id]) $rootScope.charts[task.id] = false; if($rootScope.charts[task._id]) $rootScope.charts[task.id] = false;
modalScope.taskStatus = taskStatus;
if (scopeInc) {
modalScope.saveTask = scopeInc.saveTask;
modalScope.addChecklist = scopeInc.addChecklist;
modalScope.addChecklistItem = scopeInc.addChecklistItem;
modalScope.removeChecklistItem = scopeInc.removeChecklistItem;
modalScope.swapChecklistItems = scopeInc.swapChecklistItems;
modalScope.navigateChecklist = scopeInc.navigateChecklist;
modalScope.checklistCompletion = scopeInc.checklistCompletion;
modalScope.canEdit = scopeInc.canEdit;
modalScope.updateTaskTags = scopeInc.updateTaskTags;
modalScope.obj = scopeInc.obj;
}
modalScope.cancelTaskEdit = cancelTaskEdit;
$rootScope.openModal('task-edit', {scope: modalScope, backdrop: 'static'});
} }
function cancelTaskEdit(task) { function cancelTaskEdit(task) {
@@ -289,7 +308,7 @@ angular.module('habitrpg')
function focusChecklist(task, index) { function focusChecklist(task, index) {
window.setTimeout(function(){ window.setTimeout(function(){
$('#task-'+task._id+' .checklist-form input[type="text"]')[index].focus(); $('#task-' + task._id + ' .checklist-form input[type="text"]')[index].focus();
}); });
} }

View File

@@ -329,7 +329,7 @@ angular.module('habitrpg')
}, },
readNotifications: function (notificationIds) { readNotifications: function (notificationIds) {
UserNotifications.readNotifications(notificationIds); return UserNotifications.readNotifications(notificationIds);
}, },
addTag: function(data) { addTag: function(data) {

View File

@@ -109,6 +109,7 @@
"js/controllers/tavernCtrl.js", "js/controllers/tavernCtrl.js",
"js/controllers/tasksCtrl.js", "js/controllers/tasksCtrl.js",
"js/controllers/userCtrl.js", "js/controllers/userCtrl.js",
"js/controllers/groupPlansCtrl.js",
"js/components/groupTasks/groupTasksController.js", "js/components/groupTasks/groupTasksController.js",
"js/components/groupTasks/groupTasksDirective.js", "js/components/groupTasks/groupTasksDirective.js",

View File

@@ -4,7 +4,7 @@
"titleIndex": "Habitica | Your Life The Role Playing Game", "titleIndex": "Habitica | Your Life The Role Playing Game",
"habitica": "Habitica", "habitica": "Habitica",
"habiticaLink": "<a href='http://habitica.wikia.com/wiki/Habitica' target='_blank'>Habitica</a>", "habiticaLink": "<a href='http://habitica.wikia.com/wiki/Habitica' target='_blank'>Habitica</a>",
"titleTasks": "Tasks", "titleTasks": "Tasks",
"titleAvatar": "Avatar", "titleAvatar": "Avatar",
"titleBackgrounds": "Backgrounds", "titleBackgrounds": "Backgrounds",
@@ -53,6 +53,7 @@
"help": "Help", "help": "Help",
"user": "User", "user": "User",
"market": "Market", "market": "Market",
"groupPlansTitle": "Group Plans",
"subscriberItem": "Mystery Item", "subscriberItem": "Mystery Item",
"newSubscriberItem": "New Mystery Item", "newSubscriberItem": "New Mystery Item",
"subscriberItemText": "Each month, subscribers will receive a mystery item. This is usually released about one week before the end of the month. See the wiki's 'Mystery Item' page for more information.", "subscriberItemText": "Each month, subscribers will receive a mystery item. This is usually released about one week before the end of the month. See the wiki's 'Mystery Item' page for more information.",
@@ -194,5 +195,7 @@
"you": "(you)", "you": "(you)",
"enableDesktopNotifications": "Enable Desktop Notifications", "enableDesktopNotifications": "Enable Desktop Notifications",
"online": "online", "online": "online",
"onlineCount": "<%= count %> online" "onlineCount": "<%= count %> online",
"loading": "Loading...",
"userIdRequired": "User ID is required"
} }

View File

@@ -1,224 +1,257 @@
{ {
"tavern": "Tavern Chat", "tavern": "Tavern Chat",
"innCheckOut": "Check Out of Inn", "innCheckOut": "Check Out of Inn",
"innCheckIn": "Rest in the 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.", "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...", "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", "lfgPosts": "Looking for Group (Party Wanted) Posts",
"tutorial": "Tutorial", "tutorial": "Tutorial",
"glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>", "glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
"wiki": "Wiki", "wiki": "Wiki",
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>", "wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
"reportAP": "Report a Problem", "reportAP": "Report a Problem",
"requestAF": "Request a Feature", "requestAF": "Request a Feature",
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>", "community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
"dataTool": "Data Display Tool", "dataTool": "Data Display Tool",
"resources": "Resources", "resources": "Resources",
"askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)", "askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)",
"tavernAlert1": "To report a bug, visit", "tavernAlert1": "To report a bug, visit",
"tavernAlert2": "the Report a Bug Guild", "tavernAlert2": "the Report a Bug Guild",
"moderatorIntro1": "Tavern and guild moderators are: ", "moderatorIntro1": "Tavern and guild moderators are: ",
"communityGuidelines": "Community Guidelines", "communityGuidelines": "Community Guidelines",
"communityGuidelinesRead1": "Please read our", "communityGuidelinesRead1": "Please read our",
"communityGuidelinesRead2": "before chatting.", "communityGuidelinesRead2": "before chatting.",
"party": "Party", "party": "Party",
"createAParty": "Create A Party", "createAParty": "Create A Party",
"updatedParty": "Party settings updated.", "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:", "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.", "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:", "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", "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!", "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", "create": "Create",
"userId": "User ID", "userId": "User ID",
"invite": "Invite", "invite": "Invite",
"leave": "Leave", "leave": "Leave",
"invitedTo": "Invited to <%= name %>", "invitedTo": "Invited to <%= name %>",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?", "invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
"invitationAcceptedHeader": "Your Invitation has been Accepted", "invitationAcceptedHeader": "Your Invitation has been Accepted",
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!", "invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
"joinNewParty": "Join New Party", "joinNewParty": "Join New Party",
"declineInvitation": "Decline Invitation", "declineInvitation": "Decline Invitation",
"partyLoading1": "Your party is being summoned. Please wait...", "partyLoading1": "Your party is being summoned. Please wait...",
"partyLoading2": "Your party is coming in from battle. Please wait...", "partyLoading2": "Your party is coming in from battle. Please wait...",
"partyLoading3": "Your party is gathering. Please wait...", "partyLoading3": "Your party is gathering. Please wait...",
"partyLoading4": "Your party is materializing. Please wait...", "partyLoading4": "Your party is materializing. Please wait...",
"systemMessage": "System Message", "systemMessage": "System Message",
"newMsg": "New message in \"<%= name %>\"", "newMsg": "New message in \"<%= name %>\"",
"chat": "Chat", "chat": "Chat",
"sendChat": "Send Chat", "sendChat": "Send Chat",
"toolTipMsg": "Fetch Recent Messages", "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.", "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", "syncPartyAndChat": "Sync Party and Chat",
"guildBankPop1": "Guild Bank", "guildBankPop1": "Guild Bank",
"guildBankPop2": "Gems which your guild leader can use for challenge prizes.", "guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
"guildGems": "Guild Gems", "guildGems": "Guild Gems",
"editGroup": "Edit Group", "editGroup": "Edit Group",
"newGroupName": "<%= groupType %> Name", "newGroupName": "<%= groupType %> Name",
"groupName": "Group Name", "groupName": "Group Name",
"groupLeader": "Group Leader", "groupLeader": "Group Leader",
"groupID": "Group ID", "groupID": "Group ID",
"groupDescr": "Description shown in public Guilds list (Markdown OK)", "groupDescr": "Description shown in public Guilds list (Markdown OK)",
"logoUrl": "Logo URL", "logoUrl": "Logo URL",
"assignLeader": "Assign Group Leader", "assignLeader": "Assign Group Leader",
"members": "Members", "members": "Members",
"partyList": "Order for party members in header", "partyList": "Order for party members in header",
"banTip": "Boot Member", "banTip": "Boot Member",
"moreMembers": "more members", "moreMembers": "more members",
"invited": "Invited", "invited": "Invited",
"leaderMsg": "Message from group leader (Markdown OK)", "leaderMsg": "Message from group leader (Markdown OK)",
"name": "Name", "name": "Name",
"description": "Description", "description": "Description",
"public": "Public", "public": "Public",
"inviteOnly": "Invite Only", "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!", "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", "search": "Search",
"publicGuilds": "Public Guilds", "publicGuilds": "Public Guilds",
"createGuild": "Create Guild", "createGuild": "Create Guild",
"guild": "Guild", "guild": "Guild",
"guilds": "Guilds", "guilds": "Guilds",
"guildsLink": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>", "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?", "sureKick": "Do you really want to remove this member from the party/guild?",
"optionalMessage": "Optional message", "optionalMessage": "Optional message",
"yesRemove": "Yes, remove them", "yesRemove": "Yes, remove them",
"foreverAlone": "Can't like your own message. Don't be that person.", "foreverAlone": "Can't like your own message. Don't be that person.",
"sortLevel": "Sort by level", "sortLevel": "Sort by level",
"sortRandom": "Sort randomly", "sortRandom": "Sort randomly",
"sortPets": "Sort by number of pets", "sortPets": "Sort by number of pets",
"sortName": "Sort by avatar name", "sortName": "Sort by avatar name",
"sortBackgrounds": "Sort by background", "sortBackgrounds": "Sort by background",
"sortHabitrpgJoined": "Sort by Habitica date joined", "sortHabitrpgJoined": "Sort by Habitica date joined",
"sortHabitrpgLastLoggedIn": "Sort by last time user logged in", "sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
"ascendingSort": "Sort Ascending", "ascendingSort": "Sort Ascending",
"descendingSort": "Sort Descending", "descendingSort": "Sort Descending",
"confirmGuild": "Create Guild for 4 Gems?", "confirmGuild": "Create Guild for 4 Gems?",
"leaveGroupCha": "Leave Guild challenges and...", "leaveGroupCha": "Leave Guild challenges and...",
"confirm": "Confirm", "confirm": "Confirm",
"leaveGroup": "Leave Guild?", "leaveGroup": "Leave Guild?",
"leavePartyCha": "Leave party challenges and...", "leavePartyCha": "Leave party challenges and...",
"leaveParty": "Leave party?", "leaveParty": "Leave party?",
"sendPM": "Send private message", "sendPM": "Send private message",
"send": "Send", "send": "Send",
"messageSentAlert": "Message sent", "messageSentAlert": "Message sent",
"pmHeading": "Private message to <%= name %>", "pmHeading": "Private message to <%= name %>",
"pmsMarkedRead": "Your private messages have been marked as read", "pmsMarkedRead": "Your private messages have been marked as read",
"clearAll": "Delete All Messages", "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.", "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", "optOutPopover": "Don't like private messages? Click to completely opt out",
"block": "Block", "block": "Block",
"unblock": "Un-block", "unblock": "Un-block",
"pm-reply": "Send a reply", "pm-reply": "Send a reply",
"inbox": "Inbox", "inbox": "Inbox",
"messageRequired": "A message is required.", "messageRequired": "A message is required.",
"toUserIDRequired": "A User ID is required", "toUserIDRequired": "A User ID is required",
"gemAmountRequired": "A number of gems is required", "gemAmountRequired": "A number of gems is required",
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.", "notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
"privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!", "privateMessageGiftIntro": "Hello <%= receiverName %>, <%= senderName %> has sent you ",
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.", "privateMessageGiftGemsMessage": "<%= gemAmount %> gems!",
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.", "privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
"abuseFlag": "Report violation of Community Guidelines", "cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
"abuseFlagModalHeading": "Report <%= name %> for violation?", "badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
"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>", "abuseFlag": "Report violation of Community Guidelines",
"abuseFlagModalButton": "Report Violation", "abuseFlagModalHeading": "Report <%= name %> for violation?",
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.", "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>",
"abuseAlreadyReported": "You have already reported this message.", "abuseFlagModalButton": "Report Violation",
"needsText": "Please type a message.", "abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
"needsTextPlaceholder": "Type your message here.", "abuseAlreadyReported": "You have already reported this message.",
"copyMessageAsToDo": "Copy message as To-Do", "needsText": "Please type a message.",
"messageAddedAsToDo": "Message copied as To-Do.", "needsTextPlaceholder": "Type your message here.",
"messageWroteIn": "<%= user %> wrote in <%= group %>", "copyMessageAsToDo": "Copy message as To-Do",
"taskFromInbox": "<%= from %> wrote '<%= message %>'", "messageAddedAsToDo": "Message copied as To-Do.",
"taskTextFromInbox": "Message from <%= from %>", "messageWroteIn": "<%= user %> wrote in <%= group %>",
"msgPreviewHeading": "Message Preview", "taskFromInbox": "<%= from %> wrote '<%= message %>'",
"leaderOnlyChallenges": "Only group leader can create challenges", "taskTextFromInbox": "Message from <%= from %>",
"sendGift": "Send Gift", "msgPreviewHeading": "Message Preview",
"inviteFriends": "Invite Friends", "leaderOnlyChallenges": "Only group leader can create challenges",
"inviteByEmail": "Invite by Email", "sendGift": "Send Gift",
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!", "inviteFriends": "Invite Friends",
"inviteFriendsNow": "Invite Friends Now", "inviteByEmail": "Invite by Email",
"inviteFriendsLater": "Invite Friends Later", "inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
"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.", "inviteFriendsNow": "Invite Friends Now",
"inviteExistUser": "Invite Existing Users", "inviteFriendsLater": "Invite Friends Later",
"byColon": "By:", "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.",
"inviteNewUsers": "Invite New Users", "inviteExistUser": "Invite Existing Users",
"sendInvitations": "Send Invitations", "byColon": "By:",
"invitationsSent": "Invitations sent!", "inviteNewUsers": "Invite New Users",
"invitationSent": "Invitation sent!", "sendInvitations": "Send Invitations",
"inviteAlertInfo2": "Or share this link (copy/paste):", "invitationsSent": "Invitations sent!",
"sendGiftHeading": "Send Gift to <%= name %>", "invitationSent": "Invitation sent!",
"sendGiftGemsBalance": "From <%= number %> Gems", "inviteAlertInfo2": "Or share this link (copy/paste):",
"sendGiftCost": "Total: $<%= cost %> USD", "sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftFromBalance": "From Balance", "sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftPurchase": "Purchase", "sendGiftCost": "Total: $<%= cost %> USD",
"sendGiftMessagePlaceholder": "Personal message (optional)", "sendGiftFromBalance": "From Balance",
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD", "sendGiftPurchase": "Purchase",
"battleWithFriends": "Battle Monsters With Friends", "sendGiftMessagePlaceholder": "Personal message (optional)",
"startPartyWithFriends": "Start a Party with your friends!", "sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
"startAParty": "Start a Party", "battleWithFriends": "Battle Monsters With Friends",
"addToParty": "Add someone to your party", "startPartyWithFriends": "Start a Party with your friends!",
"likePost": "Click if you like this post!", "startAParty": "Start a Party",
"partyExplanation1": "Play Habitica with friends to stay accountable!", "addToParty": "Add someone to your party",
"partyExplanation2": "Battle monsters and create Challenges!", "likePost": "Click if you like this post!",
"partyExplanation3": "Invite friends now to earn a Quest Scroll!", "partyExplanation1": "Play Habitica with friends to stay accountable!",
"wantToStartParty": "Do you want to start a party?", "partyExplanation2": "Battle monsters and create Challenges!",
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!", "partyExplanation3": "Invite friends now to earn a Quest Scroll!",
"nameYourParty": "Name your new party!", "wantToStartParty": "Do you want to start a party?",
"partyEmpty": "You're the only one in your party. Invite your friends!", "exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.", "nameYourParty": "Name your new party!",
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.", "partyEmpty": "You're the only one in your party. Invite your friends!",
"possessiveParty": "<%= name %>'s Party", "partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
"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.", "guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
"partyUpName": "Party Up", "possessiveParty": "<%= name %>'s Party",
"partyOnName": "Party On", "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.",
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.", "partyUpName": "Party Up",
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!", "partyOnName": "Party On",
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.", "partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
"groupIdRequired": "\"groupId\" must be a valid UUID", "partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
"groupNotFound": "Group not found or you don't have access.", "largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
"groupTypesRequired": "You must supply a valid \"type\" query string.", "groupIdRequired": "\"groupId\" must be a valid UUID",
"questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.", "groupNotFound": "Group not found or you don't have access.",
"cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.", "groupTypesRequired": "You must supply a valid \"type\" query string.",
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!", "questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
"memberCannotRemoveYourself": "You cannot remove yourself!", "cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
"groupMemberNotFound": "User not found among group's members", "onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
"mustBeGroupMember": "Must be member of the group.", "memberCannotRemoveYourself": "You cannot remove yourself!",
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"", "groupMemberNotFound": "User not found among group's members",
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"", "mustBeGroupMember": "Must be member of the group.",
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.", "keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
"inviteMissingEmail": "Missing email address in invite.", "keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
"inviteMissingUuid": "Missing user id in invite", "canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
"inviteMustNotBeEmpty": "Invite must not be empty.", "inviteMissingEmail": "Missing email address in invite.",
"partyMustbePrivate": "Parties must be private", "inviteMissingUuid": "Missing user id in invite",
"userAlreadyInGroup": "User already in that group.", "inviteMustNotBeEmpty": "Invite must not be empty.",
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.", "partyMustbePrivate": "Parties must be private",
"userAlreadyInvitedToGroup": "User already invited to that group.", "userAlreadyInGroup": "User already in that group.",
"userAlreadyPendingInvitation": "User already pending invitation.", "cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
"userAlreadyInAParty": "User already in a party.", "userAlreadyInvitedToGroup": "User already invited to that group.",
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.", "userAlreadyPendingInvitation": "User already pending invitation.",
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).", "userAlreadyInAParty": "User already in a party.",
"uuidsMustBeAnArray": "User ID invites must be an array.", "userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
"emailsMustBeAnArray": "Email address invites must be an array.", "userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time", "uuidsMustBeAnArray": "User ID invites must be an array.",
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!", "emailsMustBeAnArray": "Email address invites must be an array.",
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!", "canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned", "onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!", "onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
"newChatMessageTitle": "New message in <%= groupName %>", "onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"exportInbox": "Export Messages", "newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
"exportInboxPopoverTitle": "Export your messages as HTML", "newChatMessageTitle": "New message in <%= groupName %>",
"exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data", "exportInbox": "Export Messages",
"to": "To:", "exportInboxPopoverTitle": "Export your messages as HTML",
"from": "From:", "exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
"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.", "to": "To:",
"confirmAddTag": "Do you really want to add \"<%= tag %>\"?", "from": "From:",
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?", "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.",
"assignTask": "Assign Task", "confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
"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.", "confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
"claim": "Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription", "groupHomeTitle": "Home",
"yourTaskHasBeenApproved": "Your task has been approved", "assignTask": "Assign Task",
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>", "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.",
"confirmTaskApproval": "Are you sure you want to approve this task?", "claim": "Claim",
"approve": "Approve", "onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"approvalTitle": "<%= text %> for user: <%= userName %>" "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",
"groupBenefitsTitle": "How a group plan can help you",
"groupBenefitsDescription": "Weve 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 theyll automatically appear in that persons 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, theyll 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?"
}

View File

@@ -22,6 +22,7 @@ import gear from './gear';
import appearances from './appearance'; import appearances from './appearance';
import backgrounds from './appearance/backgrounds.js' import backgrounds from './appearance/backgrounds.js'
import spells from './spells'; import spells from './spells';
import subscriptionBlocks from './subscriptionBlocks';
import faq from './faq'; import faq from './faq';
import timeTravelers from './time-travelers'; import timeTravelers from './time-travelers';
@@ -33,6 +34,7 @@ api.itemList = ITEM_LIST;
api.gear = gear; api.gear = gear;
api.spells = spells; api.spells = spells;
api.subscriptionBlocks = subscriptionBlocks;
api.mystery = timeTravelers.mystery; api.mystery = timeTravelers.mystery;
api.timeTravelerStore = timeTravelers.timeTravelerStore; api.timeTravelerStore = timeTravelers.timeTravelerStore;
@@ -2808,35 +2810,6 @@ api.appearances = appearances;
api.backgrounds = backgrounds; api.backgrounds = backgrounds;
api.subscriptionBlocks = {
basic_earned: {
months: 1,
price: 5
},
basic_3mo: {
months: 3,
price: 15
},
basic_6mo: {
months: 6,
price: 30
},
google_6mo: {
months: 6,
price: 24,
discount: true,
original: 30
},
basic_12mo: {
months: 12,
price: 48
},
};
_.each(api.subscriptionBlocks, function(b, k) {
return b.key = k;
});
api.userDefaults = { api.userDefaults = {
habits: [ habits: [
{ {

View File

@@ -0,0 +1,39 @@
/* eslint-disable camelcase */
import _ from 'lodash';
let subscriptionBlocks = {
basic_earned: {
months: 1,
price: 5,
},
basic_3mo: {
months: 3,
price: 15,
},
basic_6mo: {
months: 6,
price: 30,
},
google_6mo: {
months: 6,
price: 24,
discount: true,
original: 30,
},
basic_12mo: {
months: 12,
price: 48,
},
group_monthly: {
type: 'group',
months: 1,
price: 9,
quantity: 3, // Default quantity for Stripe - The same as having 3 user subscriptions
},
};
_.each(subscriptionBlocks, function createKeys (b, k) {
return b.key = k;
});
module.exports = subscriptionBlocks;

View File

@@ -179,6 +179,8 @@ module.exports = function scoreTask (options = {}, req = {}) {
exp: user.stats.exp, exp: user.stats.exp,
}; };
if (task.group && task.group.approval && task.group.approval.required && !task.group.approval.approved) return;
// This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying // This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
// the API consumer, then cleared afterwards // the API consumer, then cleared afterwards
user._tmp = {}; user._tmp = {};

View File

@@ -21,6 +21,9 @@ import { encrypt } from '../../libs/encryption';
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications'; import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
import pusher from '../../libs/pusher'; import pusher from '../../libs/pusher';
import common from '../../../common'; import common from '../../../common';
import payments from '../../libs/payments';
import shared from '../../../common';
/** /**
* @apiDefine GroupBodyInvalid * @apiDefine GroupBodyInvalid
@@ -106,6 +109,95 @@ api.createGroup = {
}, },
}; };
/**
* @api {post} /api/v3/groups/create-plan Create a Group and then redirect to the correct payment
* @apiName CreateGroupPlan
* @apiGroup Group
*
* @apiSuccess {Object} data The created group
*/
api.createGroupPlan = {
method: 'POST',
url: '/groups/create-plan',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let group = new Group(Group.sanitize(req.body.groupToCreate));
req.checkBody('paymentType', res.t('paymentTypeRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
// @TODO: Change message
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
group.leader = user._id;
user.guilds.push(group._id);
let results = await Bluebird.all([user.save(), group.save()]);
let savedGroup = results[1];
// Analytics
let analyticsObject = {
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
};
res.analytics.track('join group', analyticsObject);
if (req.body.paymentType === 'Stripe') {
let token = req.body.id;
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
let groupId = savedGroup._id;
let email = req.body.email;
let headers = req.headers;
let coupon = req.query.coupon;
await payments.payWithStripe([
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
]);
} else if (req.body.paymentType === 'Amazon') {
let billingAgreementId = req.body.billingAgreementId;
let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
let coupon = req.body.coupon;
let groupId = savedGroup._id;
let headers = req.headers;
await payments.subscribeWithAmazon([
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
]);
}
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise
let response = savedGroup.toJSON();
// the leader is the authenticated user
response.leader = {
_id: user._id,
profile: {name: user.profile.name},
};
res.respond(201, response); // do not remove chat flags data as we've just created the group
},
};
/** /**
* @api {get} /api/v3/groups Get groups for a user * @api {get} /api/v3/groups Get groups for a user
* @apiName GetGroups * @apiName GetGroups
@@ -303,6 +395,8 @@ api.joinGroup = {
group.memberCount += 1; group.memberCount += 1;
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
let promises = [group.save(), user.save()]; let promises = [group.save(), user.save()];
if (inviter) { if (inviter) {
@@ -459,6 +553,8 @@ api.leaveGroup = {
await group.leave(user, req.query.keep); await group.leave(user, req.query.keep);
if (group.purchased.plan && group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
_removeMessagesFromMember(user, group._id); _removeMessagesFromMember(user, group._id);
await user.save(); await user.save();
@@ -535,6 +631,7 @@ api.removeGroupMember = {
if (isInGroup) { if (isInGroup) {
group.memberCount -= 1; group.memberCount -= 1;
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
if (group.quest && group.quest.leader === member._id) { if (group.quest && group.quest.leader === member._id) {
group.quest.key = undefined; group.quest.key = undefined;

View File

@@ -245,7 +245,7 @@ api.updateTask = {
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one } else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
let oldCheckList = task.checklist;
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances? // we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req); let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
@@ -254,6 +254,8 @@ api.updateTask = {
if (!challenge && task.userId && task.challenge && task.challenge.id) { if (!challenge && task.userId && task.challenge && task.challenge.id) {
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj); sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
} else if (!group && task.userId && task.group && task.group.id) {
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
} else { } else {
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj); sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
} }
@@ -270,7 +272,15 @@ api.updateTask = {
let savedTask = await task.save(); let savedTask = await task.save();
if (group && task.group.id && task.group.assignedUsers.length > 0) { if (group && task.group.id && task.group.assignedUsers.length > 0) {
await group.updateTask(savedTask); let updateCheckListItems = _.remove(sanitizedObj.checklist, function getCheckListsToUpdate (checklist) {
let indexOld = _.findIndex(oldCheckList, function findIndex (check) {
return check.id === checklist.id;
});
if (indexOld !== -1) return checklist.text !== oldCheckList[indexOld].text;
return false; // Only return changes. Adding and remove are handled differently
});
await group.updateTask(savedTask, {updateCheckListItems});
} }
res.respond(200, savedTask); res.respond(200, savedTask);
@@ -508,13 +518,16 @@ api.addChecklistItem = {
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo')); if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
task.checklist.push(Tasks.Task.sanitizeChecklist(req.body)); let newCheckListItem = Tasks.Task.sanitizeChecklist(req.body);
task.checklist.push(newCheckListItem);
let savedTask = await task.save(); let savedTask = await task.save();
newCheckListItem.id = savedTask.checklist[savedTask.checklist.length - 1].id;
res.respond(200, savedTask); res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask); if (challenge) challenge.updateTask(savedTask);
if (group && task.group.id && task.group.assignedUsers.length > 0) { if (group && task.group.id && task.group.assignedUsers.length > 0) {
await group.updateTask(savedTask); await group.updateTask(savedTask, {newCheckListItem});
} }
}, },
}; };
@@ -676,7 +689,7 @@ api.removeChecklistItem = {
res.respond(200, savedTask); res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask); if (challenge) challenge.updateTask(savedTask);
if (group && task.group.id && task.group.assignedUsers.length > 0) { if (group && task.group.id && task.group.assignedUsers.length > 0) {
await group.updateTask(savedTask); await group.updateTask(savedTask, {removedCheckListItemId: req.params.itemId});
} }
}, },
}; };
@@ -941,6 +954,8 @@ api.deleteTask = {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} else if (task.userId && task.challenge.id && !task.challenge.broken) { } else if (task.userId && task.challenge.id && !task.challenge.broken) {
throw new NotAuthorized(res.t('cantDeleteChallengeTasks')); throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
} else if (task.group.id && task.group.assignedUsers.indexOf(user._id) !== -1) {
throw new NotAuthorized(res.t('cantDeleteAssignedGroupTasks'));
} }
if (task.type !== 'todo' || !task.completed) { if (task.type !== 'todo' || !task.completed) {

View File

@@ -1,5 +1,4 @@
import { authWithHeaders } from '../../../middlewares/auth'; import { authWithHeaders } from '../../../middlewares/auth';
import ensureDevelpmentMode from '../../../middlewares/ensureDevelpmentMode';
import Bluebird from 'bluebird'; import Bluebird from 'bluebird';
import * as Tasks from '../../../models/task'; import * as Tasks from '../../../models/task';
import { model as Group } from '../../../models/group'; import { model as Group } from '../../../models/group';
@@ -22,7 +21,6 @@ let api = {};
* @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks. * @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks.
* @apiName CreateGroupTasks * @apiName CreateGroupTasks
* @apiGroup Task * @apiGroup Task
* @apiIgnore
* *
* @apiParam {UUID} groupId The id of the group the new task(s) will belong to * @apiParam {UUID} groupId The id of the group the new task(s) will belong to
* *
@@ -31,7 +29,7 @@ let api = {};
api.createGroupTasks = { api.createGroupTasks = {
method: 'POST', method: 'POST',
url: '/tasks/group/:groupId', url: '/tasks/group/:groupId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID(); req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
@@ -55,7 +53,6 @@ api.createGroupTasks = {
* @api {get} /api/v3/tasks/group/:groupId Get a group's tasks * @api {get} /api/v3/tasks/group/:groupId Get a group's tasks
* @apiName GetGroupTasks * @apiName GetGroupTasks
* @apiGroup Task * @apiGroup Task
* @apiIgnore
* *
* @apiParam {UUID} groupId The id of the group from which to retrieve the tasks * @apiParam {UUID} groupId The id of the group from which to retrieve the tasks
* @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks * @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks
@@ -65,7 +62,7 @@ api.createGroupTasks = {
api.getGroupTasks = { api.getGroupTasks = {
method: 'GET', method: 'GET',
url: '/tasks/group/:groupId', url: '/tasks/group/:groupId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID(); req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types); req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
@@ -97,7 +94,7 @@ api.getGroupTasks = {
api.assignTask = { api.assignTask = {
method: 'POST', method: 'POST',
url: '/tasks/:taskId/assign/:assignedUserId', url: '/tasks/:taskId/assign/:assignedUserId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID(); req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -120,12 +117,22 @@ api.assignTask = {
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned')); throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
} }
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); let groupFields = `${requiredGroupFields} chat`;
let group = await Group.getGroup({user, groupId: task.group.id, fields: groupFields});
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id && user._id !== assignedUserId) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (group.leader !== user._id && user._id !== assignedUserId) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
await group.syncTask(task, assignedUser); // User is claiming the task
if (user._id === assignedUserId) {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
group.sendChat(message, user);
}
let promises = [];
promises.push(group.syncTask(task, assignedUser));
promises.push(group.save());
await Bluebird.all(promises);
res.respond(200, task); res.respond(200, task);
}, },
@@ -145,7 +152,7 @@ api.assignTask = {
api.unassignTask = { api.unassignTask = {
method: 'POST', method: 'POST',
url: '/tasks/:taskId/unassign/:assignedUserId', url: '/tasks/:taskId/unassign/:assignedUserId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID(); req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -194,7 +201,7 @@ api.unassignTask = {
api.approveTask = { api.approveTask = {
method: 'POST', method: 'POST',
url: '/tasks/:taskId/approve/:userId', url: '/tasks/:taskId/approve/:userId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID(); req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -226,10 +233,15 @@ api.approveTask = {
task.group.approval.approved = true; task.group.approval.approved = true;
assignedUser.addNotification('GROUP_TASK_APPROVED', { assignedUser.addNotification('GROUP_TASK_APPROVED', {
message: res.t('yourTaskHasBeenApproved'), message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
groupId: group._id, groupId: group._id,
}); });
assignedUser.addNotification('SCORED_TASK', {
message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
scoreTask: task,
});
await Bluebird.all([assignedUser.save(), task.save()]); await Bluebird.all([assignedUser.save(), task.save()]);
res.respond(200, task); res.respond(200, task);
@@ -241,7 +253,6 @@ api.approveTask = {
* @apiVersion 3.0.0 * @apiVersion 3.0.0
* @apiName GetGroupApprovals * @apiName GetGroupApprovals
* @apiGroup Task * @apiGroup Task
* @apiIgnore
* *
* @apiParam {UUID} groupId The id of the group from which to retrieve the approvals * @apiParam {UUID} groupId The id of the group from which to retrieve the approvals
* *
@@ -250,7 +261,7 @@ api.approveTask = {
api.getGroupApprovals = { api.getGroupApprovals = {
method: 'GET', method: 'GET',
url: '/approvals/group/:groupId', url: '/approvals/group/:groupId',
middlewares: [ensureDevelpmentMode, authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID(); req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();

View File

@@ -91,6 +91,8 @@ api.checkout = {
let orderReferenceId = req.body.orderReferenceId; let orderReferenceId = req.body.orderReferenceId;
let amount = 5; let amount = 5;
// @TODO: Make thise use payment.subscribeWithAmazon
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId'); if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
if (gift) { if (gift) {

View File

@@ -49,6 +49,9 @@ api.checkout = {
let groupId = req.query.groupId; let groupId = req.query.groupId;
let coupon; let coupon;
let response; let response;
let subscriptionId;
// @TODO: Update this to use payments.payWithStripe
if (!token) throw new BadRequest('Missing req.body.id'); if (!token) throw new BadRequest('Missing req.body.id');
@@ -59,12 +62,20 @@ api.checkout = {
if (!coupon) throw new BadRequest(res.t('invalidCoupon')); if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
} }
response = await stripe.customers.create({ let customerObject = {
email: req.body.email, email: req.body.email,
metadata: { uuid: user._id }, metadata: { uuid: user._id },
card: token, card: token,
plan: sub.key, plan: sub.key,
}); };
if (groupId) {
customerObject.quantity = sub.quantity;
}
response = await stripe.customers.create(customerObject);
if (groupId) subscriptionId = response.subscriptions.data[0].id;
} else { } else {
let amount = 500; // $5 let amount = 500; // $5
@@ -91,6 +102,7 @@ api.checkout = {
sub, sub,
headers: req.headers, headers: req.headers,
groupId, groupId,
subscriptionId,
}); });
} else { } else {
let method = 'buyGems'; let method = 'buyGems';
@@ -149,7 +161,9 @@ api.subscribeEdit = {
throw new NotFound(res.t('groupNotFound')); throw new NotFound(res.t('groupNotFound'));
} }
if (!group.leader === user._id) { let allowedManagers = [group.leader, group.purchased.plan.owner];
if (allowedManagers.indexOf(user._id) === -1) {
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription')); throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
} }
customerId = group.purchased.plan.customerId; customerId = group.purchased.plan.customerId;

View File

@@ -11,11 +11,23 @@ import {
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../models/group'; } from '../models/group';
import { model as Coupon } from '../models/coupon';
import { model as User } from '../models/user';
import { import {
NotAuthorized, NotAuthorized,
NotFound, NotFound,
} from './errors'; } from './errors';
import slack from './slack'; import slack from './slack';
import nconf from 'nconf';
import stripeModule from 'stripe';
import amzLib from './amazonPayments';
import {
BadRequest,
} from './errors';
import cc from 'coupon-code';
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
let api = {}; let api = {};
@@ -49,6 +61,7 @@ api.createSubscription = async function createSubscription (data) {
let groupId; let groupId;
let itemPurchased = 'Subscription'; let itemPurchased = 'Subscription';
let purchaseType = 'subscribe'; let purchaseType = 'subscribe';
let emailType = 'subscription-begins';
// If we are buying a group subscription // If we are buying a group subscription
if (data.groupId) { if (data.groupId) {
@@ -66,7 +79,9 @@ api.createSubscription = async function createSubscription (data) {
recipient = group; recipient = group;
itemPurchased = 'Group-Subscription'; itemPurchased = 'Group-Subscription';
purchaseType = 'group-subscribe'; purchaseType = 'group-subscribe';
emailType = 'group-subscription-begins';
groupId = group._id; groupId = group._id;
recipient.purchased.plan.quantity = data.sub.quantity;
} }
plan = recipient.purchased.plan; plan = recipient.purchased.plan;
@@ -98,11 +113,16 @@ api.createSubscription = async function createSubscription (data) {
// Specify a lastBillingDate just for Amazon Payments // Specify a lastBillingDate just for Amazon Payments
// Resetted every time the subscription restarts // Resetted every time the subscription restarts
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined, lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined,
owner: data.user._id,
}).defaults({ // allow non-override if a plan was previously used }).defaults({ // allow non-override if a plan was previously used
gemsBought: 0, gemsBought: 0,
dateCreated: today, dateCreated: today,
mysteryItems: [], mysteryItems: [],
}).value(); }).value();
if (data.subscriptionId) {
plan.subscriptionId = data.subscriptionId;
}
} }
// Block sub perks // Block sub perks
@@ -119,7 +139,7 @@ api.createSubscription = async function createSubscription (data) {
} }
if (!data.gift) { if (!data.gift) {
txnEmail(data.user, 'subscription-begins'); txnEmail(data.user, emailType);
} }
analytics.trackPurchase({ analytics.trackPurchase({
@@ -208,12 +228,29 @@ 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 // Sets their subscription to be cancelled later
api.cancelSubscription = async function cancelSubscription (data) { api.cancelSubscription = async function cancelSubscription (data) {
let plan; let plan;
let group; let group;
let cancelType = 'unsubscribe'; let cancelType = 'unsubscribe';
let groupId; let groupId;
let emailType = 'cancel-subscription';
// If we are buying a group subscription // If we are buying a group subscription
if (data.groupId) { if (data.groupId) {
@@ -224,10 +261,13 @@ api.cancelSubscription = async function cancelSubscription (data) {
throw new NotFound(shared.i18n.t('groupNotFound')); throw new NotFound(shared.i18n.t('groupNotFound'));
} }
if (!group.leader === data.user._id) { let allowedManagers = [group.leader, group.purchased.plan.owner];
if (allowedManagers.indexOf(data.user._id) === -1) {
throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription')); throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription'));
} }
plan = group.purchased.plan; plan = group.purchased.plan;
emailType = 'group-cancel-subscription';
} else { } else {
plan = data.user.purchased.plan; plan = data.user.purchased.plan;
} }
@@ -252,7 +292,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
await data.user.save(); await data.user.save();
} }
txnEmail(data.user, 'cancel-subscription'); txnEmail(data.user, emailType);
if (group) { if (group) {
cancelType = 'group-unsubscribe'; cancelType = 'group-unsubscribe';
@@ -343,4 +383,180 @@ api.buyGems = async function buyGems (data) {
await data.user.save(); await data.user.save();
}; };
/**
* Allows for purchasing a user subscription, group subscription or gems with Stripe
*
* @param options
* @param options.token The stripe token generated on the front end
* @param options.user The user object who is purchasing
* @param options.gift The gift details if any
* @param options.sub The subscription data to purchase
* @param options.groupId The id of the group purchasing a subscription
* @param options.email The email enter by the user on the Stripe form
* @param options.headers The request headers to store on analytics
* @return undefined
*/
api.payWithStripe = async function payWithStripe (options, stripeInc) {
let [
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
] = options;
let response;
let subscriptionId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection
let stripeApi = stripe;
if (stripeInc) stripeApi = stripeInc;
if (!token) throw new BadRequest('Missing req.body.id');
if (sub) {
if (sub.discount) {
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
}
let customerObject = {
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
};
if (groupId) {
customerObject.quantity = sub.quantity;
}
response = await stripeApi.customers.create(customerObject);
if (groupId) subscriptionId = response.subscriptions.data[0].id;
} else {
let amount = 500; // $5
if (gift) {
if (gift.type === 'subscription') {
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
} else {
amount = `${gift.gems.amount / 4 * 100}`;
}
}
response = await stripe.charges.create({
amount,
currency: 'usd',
card: token,
});
}
if (sub) {
await this.createSubscription({
user,
customerId: response.id,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
} else {
let method = 'buyGems';
let data = {
user,
customerId: response.id,
paymentMethod: 'Stripe',
gift,
};
if (gift) {
let member = await User.findById(gift.uuid).exec();
gift.member = member;
if (gift.type === 'subscription') method = 'createSubscription';
data.paymentMethod = 'Gift';
}
await this[method](data);
}
};
/**
* Allows for purchasing a user subscription or group subscription with Amazon
*
* @param options
* @param options.billingAgreementId The Amazon billingAgreementId generated on the front end
* @param options.user The user object who is purchasing
* @param options.sub The subscription data to purchase
* @param options.coupon The coupon to discount the sub
* @param options.groupId The id of the group purchasing a subscription
* @param options.headers The request headers to store on analytics
* @return undefined
*/
api.subscribeWithAmazon = async function subscribeWithAmazon (options) {
let [
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
] = options;
if (!sub) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
if (sub.discount) { // apply discount
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
if (!result) throw new NotAuthorized(shared.i18n.t('invalidCoupon'));
}
await amzLib.setBillingAgreementDetails({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: 'Habitica Subscription',
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: shared.uuid(),
StoreName: 'Habitica',
CustomInformation: 'Habitica Subscription',
},
},
});
await amzLib.confirmBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
});
await amzLib.authorizeOnBillingAgreement({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: shared.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: 'USD',
Amount: sub.price,
},
SellerAuthorizationNote: 'Habitica Subscription Payment',
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: 'Habitica Subscription Payment',
SellerOrderAttributes: {
SellerOrderId: shared.uuid(),
StoreName: 'Habitica',
},
});
await this.createSubscription({
user,
customerId: billingAgreementId,
paymentMethod: 'Amazon Payments',
sub,
headers,
groupId,
});
};
module.exports = api; module.exports = api;

View File

@@ -137,6 +137,7 @@ async function cronAsync (req, res) {
// Clear old completed todos - 30 days for free users, 90 for subscribers // Clear old completed todos - 30 days for free users, 90 for subscribers
// Do not delete challenges completed todos TODO unless the task is broken? // Do not delete challenges completed todos TODO unless the task is broken?
// Do not delete group completed todos
Tasks.Task.remove({ Tasks.Task.remove({
userId: user._id, userId: user._id,
type: 'todo', type: 'todo',
@@ -145,6 +146,7 @@ async function cronAsync (req, res) {
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(), $lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
}, },
'challenge.id': {$exists: false}, 'challenge.id': {$exists: false},
'group.id': {$exists: false},
}).exec(); }).exec();
res.locals.wasModified = true; // TODO remove after v2 is retired res.locals.wasModified = true; // TODO remove after v2 is retired

View File

@@ -13,6 +13,7 @@ import { groupChatReceivedWebhook } from '../libs/webhook';
import { import {
InternalServerError, InternalServerError,
BadRequest, BadRequest,
NotAuthorized,
} from '../libs/errors'; } from '../libs/errors';
import baseModel from '../libs/baseModel'; import baseModel from '../libs/baseModel';
import { sendTxn as sendTxnEmail } from '../libs/email'; import { sendTxn as sendTxnEmail } from '../libs/email';
@@ -859,6 +860,11 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
let group = this; let group = this;
let update = {}; let update = {};
let plan = group.purchased.plan;
if (group.memberCount <= 1 && group.privacy === 'private' && plan && plan.customerId && !plan.dateTerminated) {
throw new NotAuthorized(shared.i18n.t('cannotDeleteActiveGroup'));
}
let challenges = await Challenge.find({ let challenges = await Challenge.find({
_id: {$in: user.challenges}, _id: {$in: user.challenges},
group: group._id, group: group._id,
@@ -899,6 +905,13 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
promises.push(group.remove()); promises.push(group.remove());
return await Bluebird.all(promises); return await Bluebird.all(promises);
} }
} else if (group.leader === user._id) { // otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
query._id = {$ne: user._id};
let seniorMember = await User.findOne(query).select('_id').exec();
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
if (seniorMember) update.$set = {leader: seniorMember._id};
} }
// otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for) // otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
update.$inc = {memberCount: -1}; update.$inc = {memberCount: -1};
@@ -915,7 +928,16 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
return await Bluebird.all(promises); return await Bluebird.all(promises);
}; };
schema.methods.updateTask = async function updateTask (taskToSync) { /**
* Updates all linked tasks for a group task
*
* @param taskToSync The group task that will be synced
* @param options.newCheckListItem The new checklist item that needs to be synced to all assigned users
* @param options.removedCheckListItem The removed checklist item that needs to be removed from all assigned users
*
* @return The created tasks
*/
schema.methods.updateTask = async function updateTask (taskToSync, options = {}) {
let group = this; let group = this;
let updateCmd = {$set: {}}; let updateCmd = {$set: {}};
@@ -926,14 +948,51 @@ schema.methods.updateTask = async function updateTask (taskToSync) {
} }
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required; updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
let taskSchema = Tasks[taskToSync.type]; let taskSchema = Tasks[taskToSync.type];
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
await taskSchema.update({ let updateQuery = {
userId: {$exists: true}, userId: {$exists: true},
'group.id': group.id, 'group.id': group.id,
'group.taskId': taskToSync._id, 'group.taskId': taskToSync._id,
}, updateCmd, {multi: true}).exec(); };
if (options.newCheckListItem) {
let newCheckList = {completed: false};
newCheckList.linkId = options.newCheckListItem.id;
newCheckList.text = options.newCheckListItem.text;
updateCmd.$push = { checklist: newCheckList };
}
if (options.removedCheckListItemId) {
updateCmd.$pull = { checklist: {linkId: {$in: [options.removedCheckListItemId]} } };
}
if (options.updateCheckListItems && options.updateCheckListItems.length > 0) {
let checkListIdsToRemove = [];
let checkListItemsToAdd = [];
options.updateCheckListItems.forEach(function gatherChecklists (updateCheckListItem) {
checkListIdsToRemove.push(updateCheckListItem.id);
let newCheckList = {completed: false};
newCheckList.linkId = updateCheckListItem.id;
newCheckList.text = updateCheckListItem.text;
checkListItemsToAdd.push(newCheckList);
});
updateCmd.$pull = { checklist: {linkId: {$in: checkListIdsToRemove} } };
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
delete updateCmd.$pull;
updateCmd.$push = { checklist: { $each: checkListItemsToAdd } };
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
return;
}
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
}; };
schema.methods.syncTask = async function groupSyncTask (taskToSync, user) { schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
@@ -952,11 +1011,13 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
if (userTags[i].name !== group.name) { if (userTags[i].name !== group.name) {
// update the name - it's been changed since // update the name - it's been changed since
userTags[i].name = group.name; userTags[i].name = group.name;
userTags[i].group = group._id;
} }
} else { } else {
userTags.push({ userTags.push({
id: group._id, id: group._id,
name: group.name, name: group.name,
group: group._id,
}); });
} }
@@ -982,6 +1043,17 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
} }
matchingTask.group.approval.required = taskToSync.group.approval.required; matchingTask.group.approval.required = taskToSync.group.approval.required;
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
// sync checklist
if (taskToSync.checklist) {
taskToSync.checklist.forEach(function syncCheckList (element) {
let newCheckList = {completed: false};
newCheckList.linkId = element.id;
newCheckList.text = element.text;
matchingTask.checklist.push(newCheckList);
});
}
if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided
if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing

View File

@@ -1,8 +1,12 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import baseModel from '../libs/baseModel'; import baseModel from '../libs/baseModel';
import validator from 'validator';
export let schema = new mongoose.Schema({ export let schema = new mongoose.Schema({
planId: String, planId: String,
subscriptionId: String,
owner: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']},
quantity: {type: Number, default: 1},
paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']} paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
customerId: String, // Billing Agreement Id in case of Amazon Payments customerId: String, // Billing Agreement Id in case of Amazon Payments
dateCreated: Date, dateCreated: Date,

View File

@@ -14,6 +14,7 @@ export let schema = new Schema({
}, },
name: {type: String, required: true}, name: {type: String, required: true},
challenge: {type: String}, challenge: {type: String},
group: {type: String},
}, { }, {
strict: true, strict: true,
minimize: false, // So empty objects are returned minimize: false, // So empty objects are returned

View File

@@ -206,6 +206,7 @@ let dailyTodoSchema = () => {
text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation
_id: false, _id: false,
id: {type: String, default: shared.uuid, required: true, validate: [validator.isUUID, 'Invalid uuid.']}, id: {type: String, default: shared.uuid, required: true, validate: [validator.isUUID, 'Invalid uuid.']},
linkId: {type: String},
}], }],
}; };
}; };

View File

@@ -16,6 +16,7 @@ const NOTIFICATION_TYPES = [
'GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVED',
'LOGIN_INCENTIVE', 'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED', 'GROUP_INVITE_ACCEPTED',
'SCORED_TASK',
]; ];
const Schema = mongoose.Schema; const Schema = mongoose.Schema;

View File

@@ -34,8 +34,9 @@
button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})') button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})')
span.glyphicon.glyphicon-trash span.glyphicon.glyphicon-trash
ul(ng-if='!_editing', hrpg-sort-tags) ul(ng-if='!_editing', hrpg-sort-tags)
li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: tag.challenge}', ng-repeat='tag in user.tags', bindonce='user.tags') li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: showChallengeClass(tag)}', ng-repeat='tag in user.tags', bindonce='user.tags')
a(ng-click='toggleFilter(tag)') a(ng-click='toggleFilter(tag)')
span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge") span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge")
markdown(text='tag.name') markdown(text='tag.name')
| {{tag}}
// <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}"> // <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}">

View File

@@ -366,65 +366,5 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
span=env.t('unsubscribeAllEmails') span=env.t('unsubscribeAllEmails')
small=env.t('unsubscribeAllEmailsText') small=env.t('unsubscribeAllEmailsText')
script(id='partials/options.settings.subscription.html',type='text/ng-template')
//-h2=env.t('individualSub')
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
h3= env.t('benefits')
.row
.col-md-6
+subPerks()
.container-fluid.slight-vertical-padding(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') include ./settings/subscription
h4=env.t('subscribeUsing')
.row.text-center
.col-xs-12.col-md-3
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
.col-xs-12.col-md-3
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-12.col-md-3
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'))
.col-md-6
table.table.alert.alert-info(ng-if='user.purchased.plan.customerId')
tr(ng-if='user.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
h4(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
.form-group.reduce-top-margin
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"')
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
span(ng-show='block.original')
span.label.label-success.line-through
| ${{:: block.original }}
=env.t('subscriptionRateText', {price:'{{::block.price}}', months: '{{::block.months}}'})
span(ng-hide='block.original')
=env.t('subscriptionRateText', {price: '{{::block.price}}', months: '{{block.months}}'})
.form-inline
.form-group
input.form-control(type='text', ng-model='_subscription.coupon', placeholder= env.t('couponPlaceholder'))
.form-group
button.pull-right.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)')= env.t("apply")
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')

View File

@@ -0,0 +1,63 @@
script(id='partials/options.settings.subscription.html',type='text/ng-template', ng-init="groupPane = 'subscription'")
//-h2=env.t('individualSub')
//- +groupSubscription
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
h3= env.t('benefits')
.row
.col-md-6
+subPerks()
.col-md-6
table.table.alert.alert-info(ng-if='user.purchased.plan.customerId')
tr(ng-if='user.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
h4(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
.form-group.reduce-top-margin
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"', ng-if="block.type !== 'group'")
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
span(ng-show='block.original')
span.label.label-success.line-through
| ${{:: block.original }}
=env.t('subscriptionRateText', {price:'{{::block.price}}', months: '{{::block.months}}'})
span(ng-hide='block.original')
=env.t('subscriptionRateText', {price: '{{::block.price}}', months: '{{block.months}}'})
.form-inline
.form-group
input.form-control(type='text', ng-model='_subscription.coupon', placeholder= env.t('couponPlaceholder'))
.form-group
button.pull-right.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)')= env.t("apply")
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')
.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
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
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
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'))

View File

@@ -1,28 +0,0 @@
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
.form-group
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
.form-group
label(for='new-group-description')=env.t('description')
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')
.form-group(ng-show='type=="guild"')
.radio
label
input(type='radio', name='new-group-privacy', value='public', ng-model='newGroup.privacy')
=env.t('public')
.radio
label
input(type='radio', name='new-group-privacy', value='private', ng-model='newGroup.privacy')
=env.t('inviteOnly')
br
input.btn.btn-default(type='submit', ng-disabled='!newGroup.privacy && !newGroup.name', value=env.t('create'))
span.gem-cost= '4 ' + env.t('gems')
p
small=env.t('gemCost')
.form-group
.checkbox
label
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
=env.t('leaderOnlyChallenges')
.form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create'))

View File

@@ -1,3 +1,6 @@
include ./groups/group-subscription
include ./groups/group-plans-benefits
a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter', popover-title=env.t('guildBankPop1'), popover=env.t('guildBankPop2'), popover-placement='left') a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter', popover-title=env.t('guildBankPop1'), popover=env.t('guildBankPop2'), popover-placement='left')
// <span class="task-action-btn tile flush bright add-gems-btn"></span> // <span class="task-action-btn tile flush bright add-gems-btn"></span>
span.task-action-btn.tile.flush.neutral span.task-action-btn.tile.flush.neutral
@@ -6,208 +9,174 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
=' ' + env.t('guildGems') =' ' + env.t('guildGems')
.container-fluid .container-fluid
.row ul.options-menu(ng-show='group.privacy === "private"')
.col-md-4 li
a(ng-click="groupPanel = 'chat'")=env.t('groupHomeTitle')
// ------ Bosses ------- li(ng-show='group.purchased.active')
+boss(false, false) a(ng-click="groupPanel = 'tasks'")=env.t('groupTasksTitle')
li(ng-show='group.purchased.active && group.leader._id === user._id')
// ------ Information ------- a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
.panel.panel-default li
.panel-heading(bindonce='group') a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('subscription')
h3.panel-title a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && !group.purchased.plan.customerId')=env.t('upgradeTitle')
span {{group.name}}
span.group-leave-join(ng-if='group')
a.btn.btn-sm.btn-danger.pull-right(ng-if="isMemberOfGroup(User.user._id, group)", ng-hide='group._editing', ng-click='clickLeave(group, $event)')
span.glyphicon.glyphicon-ban-circle
=env.t('leave')
a.btn.btn-success.pull-right(ng-if='!isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
span(ng-if='group.leader._id == user.id')
button.btn.btn-sm.btn-primary.pull-right(ng-click='cancelEdit(group)', ng-hide='!group._editing')=env.t('cancel')
button.btn.btn-sm.btn-primary.pull-right(ng-click='saveEdit(group)', ng-show='group._editing')=env.t('save')
button.btn.btn-sm.btn-default.pull-right(ng-click='editGroup(group)', ng-hide='group._editing')=env.t('editGroup')
.panel-body
form(ng-show='group._editing')
.form-group
label=env.t('groupName')
input.form-control(type='text', ng-model='groupCopy.name', placeholder=env.t('groupName'))
.form-group
label=env.t('description')
textarea.form-control(rows=6, ng-model='groupCopy.description')
include ../../shared/formatting-help
.form-group
label=env.t('logoUrl')
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='groupCopy.logo')
.form-group
.checkbox
label
input(type='checkbox', ng-model='groupCopy.leaderOnly.challenges')
=env.t('leaderOnlyChallenges')
h4=env.t('assignLeader')
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
div(ng-show='!group._editing')
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
markdown(text='group.description')
br
p=env.t('groupLeader')
|:
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
| {{group.leader.profile.name}}
.text-center(ng-if='group.type === "party"')
.row.row-margin: .col-sm-6.col-sm-offset-3
button.btn.btn-success.btn-block(
ng-if='!group.quest.key',
ng-click='clickStartQuest();'
)=env.t('startAQuest')
// ------ Members -------
.panel.panel-default
.panel-heading
h3.panel-title
=env.t('members')
span(ng-if='group.type=="party" && (group.onlineUsers || group.onlineUsers == 0)')= ' (' + env.t('onlineCount', {count: "{{group.onlineUsers}}"}) + ')'
button.pull-right.btn.btn-primary(ng-click="openInviteModal(group)")=env.t("inviteFriends")
.panel-body.modal-fixed-height
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
tr(ng-repeat='member in group.members track by member._id')
td.media
// allow leaders to ban members
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
a.media-object(ng-click='removeMember(group, member, true)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(member._id, true)')
| {{member.profile.name}}
span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
| (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
.pull-right(ng-if='group.type === "party"')
span.text-success {{member.online ? '&#9679 ' + env.t('online') : ''}}
tr(ng-if='::group.memberCount > group.members.length')
td
span.badge {{group.memberCount - group.members.length}}
= ' ' + env.t('moreMembers')
h4(ng-show='group.invites.length > 0')=env.t('invited')
table.table.table-striped
tr(ng-repeat='invite in group.invites')
td.media
// allow leaders to ban members
.pull-left(ng-show='group.leader._id == user.id')
a.media-object(ng-click='removeMember(group, invite, false)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(invite._id, true)')
| {{invite.profile.name}}
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
.panel-heading
h3.panel-title=env.t('partyList')
.panel-body
.form-group
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
| &nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
include ./challenge-box
div(ng-if="group")
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
.slight-vertical-padding
small.muted=env.t('groupID')
| : {{group._id}}
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
small.muted=env.t('userId')
| : {{user._id}}
.col-md-8
div
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
.slight-vertical-padding
table(ng-show='group.leaderMessage')
tr
td
.popover.static-popover.fade.right.in.wide-popover
.arrow
h3.popover-title {{group.leader.profile.name}}
.popover-content
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
ul.options-menu(ng-init="groupPane = 'chat'", ng-hide="true")
//- li
//- a(ng-click="groupPane = 'chat'")=env.t('chat')
//- li
//- a(ng-click="groupPane = 'tasks'", ng-show='group.purchased.active')=env.t('tasks')
//- li
//- a(ng-click="groupPane = 'approvals'", ng-show='group.purchased.active && group.leader._id === user._id')=env.t('approvals')
//- li
//- a(ng-click="groupPane = 'subscription'", ng-show='group.leader._id === user._id')=env.t('subscription')
.tab-content .tab-content
.tab-pane.active .tab-pane.active
div(ng-controller='ChatCtrl', ng-show="groupPane == 'chat'") .row(ng-show="groupPanel == 'chat'")
.alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote') .col-md-4
h3=env.t('chat')
include ./chat-box
+chatMessages() // ------ Bosses -------
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty') +boss(false, false)
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
group-tasks(ng-show="groupPane == 'tasks'")
group-approvals(ng-show="groupPane == 'approvals'", ng-if="group.leader._id === user._id", group="group")
//TODO: This can be a directive and the group/user can be an object passed via attribute
div(ng-show="groupPane == 'subscription'")
.col-md-12
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!group.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='group.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[group.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[group.purchased.plan.planId].months}}', plan: '{{group.purchased.plan.paymentMethod}}'})
tr(ng-if='group.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{group.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li #{env.t('consecutiveMonths')} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{group.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{group.purchased.plan.consecutive.trinkets}}
div(ng-if='group.purchased.plan.customerId') // ------ Information -------
.btn.btn-primary(ng-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit({groupId: group.id})')=env.t('subUpdateCard') .panel.panel-default
.btn.btn-sm.btn-danger(ng-if='!group.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription({group: group})')=env.t('cancelSub') .panel-heading(bindonce='group')
h3.panel-title
span {{group.name}}
span.group-leave-join(ng-if='group')
a.btn.btn-sm.btn-danger.pull-right(ng-if="isMemberOfGroup(User.user._id, group)", ng-hide='group._editing', ng-click='clickLeave(group, $event)')
span.glyphicon.glyphicon-ban-circle
=env.t('leave')
a.btn.btn-success.pull-right(ng-if='!isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
span(ng-if='group.leader._id == user.id')
button.btn.btn-sm.btn-primary.pull-right(ng-click='cancelEdit(group)', ng-hide='!group._editing')=env.t('cancel')
button.btn.btn-sm.btn-primary.pull-right(ng-click='saveEdit(group)', ng-show='group._editing')=env.t('save')
button.btn.btn-sm.btn-default.pull-right(ng-click='editGroup(group)', ng-hide='group._editing')=env.t('editGroup')
.container-fluid.slight-vertical-padding(ng-if='!group.purchased.plan.customerId || (group.purchased.plan.customerId && group.purchased.plan.dateTerminated)', ng-init="_subscription.key='group_monthly'") .panel-body
small.muted=env.t('subscribeUsing') form(ng-show='group._editing')
.row.text-center .form-group
.col-xs-4 label=env.t('groupName')
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') input.form-control(type='text', ng-model='groupCopy.name', placeholder=env.t('groupName'))
.col-xs-4 .form-group
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') label=env.t('description')
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal')) textarea.form-control(rows=6, ng-model='groupCopy.description')
.col-xs-4 include ../../shared/formatting-help
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})") .form-group
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments')) label=env.t('logoUrl')
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='groupCopy.logo')
.form-group
.checkbox
label
input(type='checkbox', ng-model='groupCopy.leaderOnly.challenges')
=env.t('leaderOnlyChallenges')
h4=env.t('assignLeader')
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
div(ng-show='!group._editing')
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
markdown(text='group.description')
br
p=env.t('groupLeader')
|:
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
| {{group.leader.profile.name}}
.text-center(ng-if='group.type === "party"')
.row.row-margin: .col-sm-6.col-sm-offset-3
button.btn.btn-success.btn-block(
ng-if='!group.quest.key',
ng-click='clickStartQuest();'
)=env.t('startAQuest')
// ------ Members -------
.panel.panel-default
.panel-heading
h3.panel-title
=env.t('members')
span(ng-if='group.type=="party" && (group.onlineUsers || group.onlineUsers == 0)')= ' (' + env.t('onlineCount', {count: "{{group.onlineUsers}}"}) + ')'
button.pull-right.btn.btn-primary(ng-click="openInviteModal(group)")=env.t("inviteFriends")
.panel-body.modal-fixed-height
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
tr(ng-repeat='member in group.members track by member._id')
td.media
// allow leaders to ban members
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
a.media-object(ng-click='removeMember(group, member, true)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(member._id, true)')
| {{member.profile.name}}
span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
| (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
.pull-right(ng-if='group.type === "party"')
span.text-success {{member.online ? '&#9679 ' + env.t('online') : ''}}
tr(ng-if='::group.memberCount > group.members.length')
td
span.badge {{group.memberCount - group.members.length}}
= ' ' + env.t('moreMembers')
h4(ng-show='group.invites.length > 0')=env.t('invited')
table.table.table-striped
tr(ng-repeat='invite in group.invites')
td.media
// allow leaders to ban members
.pull-left(ng-show='group.leader._id == user.id')
a.media-object(ng-click='removeMember(group, invite, false)')
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
a.media-body
span(ng-click='clickMember(invite._id, true)')
| {{invite.profile.name}}
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
.panel-heading
h3.panel-title=env.t('partyList')
.panel-body
.form-group
select.form-control#partyOrder(
ng-model='user.party.order',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderChoices',
ng-change='set({"party.order": user.party.order})'
)
| &nbsp;
select.form-control#partyOrderAscending(
ng-model='user.party.orderAscending',
ng-controller='ChatCtrl',
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
ng-change='set({"party.orderAscending": user.party.orderAscending})'
)
include ./challenge-box
div(ng-if="group")
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
.slight-vertical-padding
small.muted=env.t('groupID')
| : {{group._id}}
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
small.muted=env.t('userId')
| : {{user._id}}
.col-md-8
div
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
.slight-vertical-padding
table(ng-show='group.leaderMessage')
tr
td
.popover.static-popover.fade.right.in.wide-popover
.arrow
h3.popover-title {{group.leader.profile.name}}
.popover-content
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
div(ng-controller='ChatCtrl')
.alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote')
h3=env.t('chat')
include ./chat-box
+chatMessages()
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
group-tasks(ng-show="groupPanel == 'tasks'")
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
+groupSubscription

View File

@@ -0,0 +1,29 @@
mixin groupCreateForm()
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
.form-group
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
.form-group
label(for='new-group-description')=env.t('description')
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')
.form-group(ng-show='type=="guild"')
.radio
label
input(type='radio', name='new-group-privacy', value='public', ng-model='newGroup.privacy')
=env.t('public')
.radio
label
input(type='radio', name='new-group-privacy', value='private', ng-model='newGroup.privacy')
=env.t('inviteOnly')
br
input.btn.btn-default(type='submit', ng-disabled='!newGroup.privacy && !newGroup.name', value=env.t('create'))
span.gem-cost= '4 ' + env.t('gems')
p
small=env.t('gemCost')
.form-group
.checkbox
label
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
=env.t('leaderOnlyChallenges')
.form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create'))

View File

@@ -0,0 +1,31 @@
mixin groupPlansBenefits()
h2.text-center=env.t('groupBenefitsTitle')
.row(style="font-size: 2rem;")
.col-md-6.col-md-offset-3=env.t('groupBenefitsDescription')
.row
.col-md-5.col-md-offset-4
div
h3
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
=env.t('groupBenefitOneTitle')
span=env.t('groupBenefitOneDescription')
div
h3
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
=env.t('groupBenefitTwoTitle')
span=env.t('groupBenefitTwoDescription')
div
h3
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
=env.t('groupBenefitThreeTitle')
span=env.t('groupBenefitThreeDescription')
div
h3
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
=env.t('groupBenefitFourTitle')
span=env.t('groupBenefitFourDescription')
div
h3
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
=env.t('groupBenefitFiveTitle')
span=env.t('groupBenefitFiveDescription')

View File

@@ -0,0 +1,40 @@
include ./create-group
script(type='text/ng-template', id='partials/options.social.groupPlans.html')
div(ng-show='activePage === PAGES.BENEFITS')
+groupPlansBenefits
br
br
.row
.col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="changePage(PAGES.CREATE_GROUP)")=env.t('createAGroup')
div(ng-show='activePage === PAGES.CREATE_GROUP')
h2.text-center=env.t('createAGroup')
.col-xs-12
+groupCreateForm
br
br
.row
.col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="createGroup()", ng-disabled="!newGroupIsReady()")=env.t('create')
div(ng-show='activePage === PAGES.UPGRADE_GROUP')
h2.text-center=env.t('upgradeTitle')
.row.text-center
.col-md-6.col-md-offset-3
a.purchase.btn.btn-primary(ng-click='upgradeGroup(PAYMENTS.STRIPE)')=env.t('card')
a.purchase(ng-click='upgradeGroup(PAYMENTS.AMAZON)')
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')
//- img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
.row
.col-md-6.col-md-offset-3
br
.text-center=env.t('groupSubscriptionPrice')

View File

@@ -0,0 +1,50 @@
// @TODO: This can be a directive and the group/user can be an object passed via attribute
mixin groupSubscription()
div(ng-show="groupPanel == 'subscription'")
.col-md-12
.col-md-12
div(ng-hide="group.purchased.plan.customerId")
+groupPlansBenefits
.row
.col-md-12
br
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!group.purchased.plan.dateTerminated'): td
h3=env.t('subscribed')
p(ng-if='group.purchased.plan.planId')=env.t('groupSubscriptionPrice')
tr(ng-if='group.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{env.t('purchasedPlanExtraMonths', {months: '{{group.purchased.plan.extraMonths | number:2}}'})}
tr(ng-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset'): td
span.glyphicon.glyphicon-forward
| &nbsp;#{env.t('consecutiveSubscription')}
ul.list-unstyled
li #{env.t('consecutiveMonths')} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
li #{env.t('gemCapExtra')} {{group.purchased.plan.consecutive.gemCapExtra}}
li #{env.t('mysticHourglasses')} {{group.purchased.plan.consecutive.trinkets}}
div(ng-if='group.purchased.plan.customerId')
.btn.btn-primary(ng-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit({groupId: group.id})')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!group.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription({group: group})')=env.t('cancelSub')
.row
.col-md-12(ng-if='!group.purchased.plan.customerId || (group.purchased.plan.customerId && group.purchased.plan.dateTerminated)', ng-init="_subscription.key='group_monthly'")
.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})")
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')
//- img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
.row(ng-if='!group.purchased.plan.customerId')
.col-md-6.col-md-offset-3
br
.text-center=env.t('groupSubscriptionPrice')

View File

@@ -1,6 +1,13 @@
script(type='text/ng-template', id='partials/groups.tasks.approvals.html') script(type='text/ng-template', id='partials/groups.tasks.approvals.html')
.panel-group(ng-repeat="approval in approvals") .row(style="margin-bottom: 2rem;")
.col-md-12
button.btn.btn-primary(ng-click='refreshApprovals()', ng-hide="loading")=env.t('refreshApprovals')
button.btn.btn-primary(ng-disabled="true", ng-show="loading")=env.t('loading')
.well(ng-show="group.approvals.length === 0")=env.t('blankApprovalsDescription')
.panel-group(ng-repeat="approval in group.approvals")
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
span {{approvalTitle(approval)}} span {{approvalTitle(approval)}}
a.btn.btn-sm.btn-success.pull-right(ng-click="approve(approval.group.taskId, approval.userId._id)")=env.t('approve') a.btn.btn-sm.btn-success.pull-right(ng-click="approve(approval.group.taskId, approval.userId._id, approval.userId.profile.name, $index)")=env.t('approve')

View File

@@ -4,4 +4,10 @@ include ./group-members-autocomplete
include ./group-tasks-approvals include ./group-tasks-approvals
script(type='text/ng-template', id='partials/groups.tasks.html') script(type='text/ng-template', id='partials/groups.tasks.html')
habitrpg-tasks(main=false) .row(style="margin-bottom: 2rem;")
.col-md-12
button.btn.btn-primary(ng-click='refreshTasks()', ng-hide="loading")=env.t('refreshGroupTasks')
button.btn.btn-primary(ng-disabled="true", ng-show="loading")=env.t('loading')
.row
habitrpg-tasks(main=false)

View File

@@ -7,6 +7,8 @@ include ./quests/index
include ./chat-message include ./chat-message
include ./party include ./party
include ./groups/group-tasks include ./groups/group-tasks
include ./groups/group-plans
include ./groups/create-group
script(type='text/ng-template', id='partials/options.social.inbox.html') script(type='text/ng-template', id='partials/options.social.inbox.html')
.options-blurbmenu .options-blurbmenu
@@ -65,7 +67,7 @@ script(type='text/ng-template', id='partials/options.social.guilds.detail.html')
script(type='text/ng-template', id='partials/options.social.guilds.create.html') script(type='text/ng-template', id='partials/options.social.guilds.create.html')
div.col-xs-12 div.col-xs-12
include ./create-group +groupCreateForm
script(type='text/ng-template', id='partials/options.social.guilds.html') script(type='text/ng-template', id='partials/options.social.guilds.html')
ul.options-submenu ul.options-submenu
@@ -105,6 +107,9 @@ script(type='text/ng-template', id='partials/options.social.html')
li(ng-class="{ active: $state.includes('options.social.hall') }") li(ng-class="{ active: $state.includes('options.social.hall') }")
a(ui-sref='options.social.hall.heroes') a(ui-sref='options.social.hall.heroes')
=env.t('hall') =env.t('hall')
li(ng-class="{ active: $state.includes('options.social.groupPlans') }")
a(ui-sref='options.social.groupPlans')
=env.t('groupPlansTitle')
.tab-content .tab-content
.tab-pane.active .tab-pane.active

View File

@@ -36,6 +36,8 @@ nav.toolbar(ng-controller='MenuCtrl')
a(ui-sref='options.social.challenges')=env.t('challenges') a(ui-sref='options.social.challenges')=env.t('challenges')
li li
a(ui-sref='options.social.hall.heroes')=env.t('hall') a(ui-sref='options.social.hall.heroes')=env.t('hall')
li
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
ul.toolbar-submenu ul.toolbar-submenu
li li
a(ui-sref='options.inventory.drops')=env.t('market') a(ui-sref='options.inventory.drops')=env.t('market')
@@ -120,6 +122,8 @@ nav.toolbar(ng-controller='MenuCtrl')
a(ui-sref='options.social.challenges')=env.t('challenges') a(ui-sref='options.social.challenges')=env.t('challenges')
li li
a(ui-sref='options.social.hall.heroes')=env.t('hall') a(ui-sref='options.social.hall.heroes')=env.t('hall')
li
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
li.toolbar-button-dropdown li.toolbar-button-dropdown
a(ui-sref='options.inventory.drops', data-close-menu) a(ui-sref='options.inventory.drops', data-close-menu)
span=env.t('inventory') span=env.t('inventory')

View File

@@ -24,6 +24,7 @@ include ./enable-desktop-notifications.jade
include ./login-incentives.jade include ./login-incentives.jade
include ./login-incentives-reward-unlocked.jade include ./login-incentives-reward-unlocked.jade
include ./generic.jade include ./generic.jade
include ./tasks-edit.jade
//- Settings //- Settings
script(type='text/ng-template', id='modals/change-day-start.html') script(type='text/ng-template', id='modals/change-day-start.html')

View File

@@ -0,0 +1,7 @@
script(type='text/ng-template', id='modals/task-edit.html')
.modal-content.task-modal(style='min-width:28em', class='{{taskStatus}}', id="task-{{task._id}}")
.modal-body.text-center(style='padding-bottom:0')
| {{scope}}
include ../tasks/edit/index
.modal-footer(style='margin-top:0')
.container-fluid

View File

@@ -1,6 +1,7 @@
div(ng-if='task._editing') div(ng-if='task._editing')
.task-options .task-options
h2 {{task._edit.text}}
// Broken Challenge // Broken Challenge
.well(ng-if='task.challenge.broken') .well(ng-if='task.challenge.broken')
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND"') div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND"')
@@ -33,7 +34,7 @@ div(ng-if='task._editing')
include ./checklist include ./checklist
form(ng-submit='saveTask(task,false,true)') form
include ./text_notes include ./text_notes
include ./habits/plus_minus include ./habits/plus_minus
@@ -49,4 +50,11 @@ div(ng-if='task._editing')
include ./advanced_options include ./advanced_options
.save-close .save-close
button(type='submit')=env.t('saveAndClose') button(type='submit', ng-click='saveTask(task,false,true); $close()')=env.t('saveAndClose')
br
br
.save-close
button(ng-click='cancelTaskEdit(task); $close()')=env.t('cancel')

View File

@@ -1,5 +1,5 @@
fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') fieldset.option-group(ng-if='!$state.includes("options.social.challenges") && !obj.leader')
p.option-title.mega(ng-class='{active: task._tags}', ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags') p.option-title.mega(ng-class='{active: task._tags}', ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags')
label.checkbox(ng-repeat='tag in user.tags', ng-if='task._tags') label.checkbox(ng-repeat='tag in user.tags', ng-if='task._tags', style='text-align: left;')
input(type='checkbox', ng-checked="task.tags.indexOf(tag.id) !== -1", ng-click="updateTaskTags(tag.id, task)") input(type='checkbox', ng-checked="task.tags.indexOf(tag.id) !== -1", ng-click="updateTaskTags(tag.id, task)")
markdown(text='tag.name') markdown(text='tag.name')

View File

@@ -6,7 +6,7 @@ include ./task_view/mixins
script(id='templates/habitrpg-tasks.html', type="text/ng-template") script(id='templates/habitrpg-tasks.html', type="text/ng-template")
.tasks-lists.container-fluid .tasks-lists.container-fluid
.row .row
.col-md-3.col-sm-6(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-md": list.type==="todo", "col-md-3": obj.type }') .col-sm-6(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-md": list.type==="todo", "col-md-3": !obj.type }')
.task-column(class='{{::list.type}}s') .task-column(class='{{::list.type}}s')
include ./task_view/graph include ./task_view/graph

View File

@@ -3,6 +3,13 @@
// Due Date // Due Date
span(ng-if='task.type=="todo" && task.date') span(ng-if='task.type=="todo" && task.date')
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}} span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}}
// Approval requested
| &nbsp;
span(ng-show='task.group.approval.requested && !task.group.approval.approved')
span(tooltip=env.t('approvalRequested'))
span=env.t('approvalRequested')
| &nbsp;
// Streak // Streak
| &nbsp; | &nbsp;
@@ -24,18 +31,21 @@
a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', tooltip=env.t('expandCollapse')) a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', tooltip=env.t('expandCollapse'))
|{{checklistCompletion(task.checklist)}}/{{task.checklist.length}} |{{checklistCompletion(task.checklist)}}/{{task.checklist.length}}
span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)')
// edit // edit
a(ng-hide='task._editing', ng-click='editTask(task, user)', tooltip=env.t('edit')) a(ng-hide='task._editing || (checkGroupAccess && !checkGroupAccess(obj))', ng-click='editTask(task, user, Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main))', tooltip=env.t('edit'))
| &nbsp; | &nbsp;
span.glyphicon.glyphicon-pencil(ng-hide='task._editing') span.glyphicon.glyphicon-pencil(ng-hide='task._editing')
| &nbsp; | &nbsp;
a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel')) a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel'))
span.glyphicon.glyphicon-remove(ng-hide='!task._editing') span.glyphicon.glyphicon-remove(ng-hide='!task._editing')
| &nbsp; | &nbsp;
// save // save
a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save')) a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save'))
span.glyphicon.glyphicon-ok(ng-hide='!task._editing') span.glyphicon.glyphicon-ok(ng-hide='!task._editing')
| &nbsp; | &nbsp;
//challenges //challenges
span(ng-if='task.challenge.id') span(ng-if='task.challenge.id')
span(ng-if='task.challenge.broken') span(ng-if='task.challenge.broken')
@@ -44,8 +54,9 @@
span(ng-if='!task.challenge.broken') span(ng-if='!task.challenge.broken')
span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge')) span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge'))
| &nbsp; | &nbsp;
// delete // delete
a(ng-if='!task.challenge.id || obj.leader._id === User.user._id', ng-click='removeTask(task, obj)', tooltip=env.t('delete')) a(ng-if='!task.challenge.id || obj.leader._id === User.user._id', ng-hide="(checkGroupAccess && !checkGroupAccess(obj))" ng-click='removeTask(task, obj)', tooltip=env.t('delete'))
span.glyphicon.glyphicon-trash span.glyphicon.glyphicon-trash
| &nbsp; | &nbsp;

View File

@@ -5,13 +5,11 @@ li(id='task-{{::task._id}}',
ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)',
ng-show='shouldShow(task, list, user.preferences)', ng-show='shouldShow(task, list, user.preferences)',
popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}',
data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}") data-popover-html="{{::taskPopover(task) | markdown}}")
ng-form(name='taskForm') ng-form(name='taskForm')
include ./meta_controls include ./meta_controls
include ./task_view/index include ./task_view/index
include ./edit/index
div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]') div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]')

View File

@@ -1,4 +1,4 @@
form.task-add(name='new{{list.type}}form', ng-hide='obj._locked', ng-submit='addTask(list, obj)', novalidate) form.task-add(name='new{{list.type}}form', ng-hide='obj._locked || (checkGroupAccess && !checkGroupAccess(obj))', ng-submit='addTask(list, obj)', novalidate)
textarea(rows='6', focus-element='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(list, obj)"}', required) textarea(rows='6', focus-element='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(list, obj)"}', required)
input(type='text', focus-element='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required) input(type='text', focus-element='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required)
button(type='submit', ng-disabled='new{{list.type}}form.$invalid') button(type='submit', ng-disabled='new{{list.type}}form.$invalid')

View File

@@ -27,7 +27,7 @@
span.reward-cost {{task.value}} span.reward-cost {{task.value}}
// Daily & Todos // Daily & Todos
span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"') span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"', ng-class='{"group-yesno": !!obj.leader}')
input.task-input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task._id}}', type='checkbox', input.task-input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task._id}}', type='checkbox',
ng-model='task.completed', ng-if='$state.includes("tasks")', ng-model='task.completed', ng-if='$state.includes("tasks")',
ng-change='changeCheck(task)' ng-change='changeCheck(task)'