Initial Spark Install

This commit is contained in:
Deon George
2017-11-03 16:26:07 +11:00
commit b1a5807eb3
766 changed files with 128896 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
window.braintreeCheckout = [];
module.exports = {
methods: {
/**
* Configure the Braintree container.
*/
braintree(containerName, callback) {
braintree.setup(Spark.braintreeToken, 'dropin', {
container: containerName,
paypal: {
singleUse: false,
locale: 'en_us',
enableShippingAddress: false
},
dataCollector: {
paypal: true
},
onReady(checkout) {
window.braintreeCheckout[containerName] = checkout;
},
onPaymentMethodReceived: callback
});
},
/**
* Reset the Braintree container.
*/
resetBraintree(containerName, callback) {
window.braintreeCheckout[containerName].teardown(() => {
window.braintreeCheckout[containerName] = null;
this.braintree(containerName, callback);
});
}
}
};

View File

@@ -0,0 +1,96 @@
module.exports = {
methods: {
/**
* Get the current discount for the given billable entity.
*/
getCurrentDiscountForBillable(type, billable) {
if (type === 'user') {
return this.getCurrentDiscountForUser(billable);
} else {
return this.getCurrentDiscountForTeam(billable);
}
},
/**
* Get the current discount for the user.
*/
getCurrentDiscountForUser(user) {
this.currentDiscount = null;
this.loadingCurrentDiscount = true;
axios.get(`/coupon/user/${user.id}`)
.then(response => {
if (response.status == 200) {
this.currentDiscount = response.data;
}
this.loadingCurrentDiscount = false;
});
},
/**
* Get the current discount for the team.
*/
getCurrentDiscountForTeam(team) {
this.currentDiscount = null;
this.loadingCurrentDiscount = true;
axios.get(`/coupon/${Spark.teamString}/${team.id}`)
.then(response => {
if (response.status == 200) {
this.currentDiscount = response.data;
}
this.loadingCurrentDiscount = false;
});
},
/**
* Get the formatted discount amount for the given discount.
*/
formattedDiscount(discount) {
if ( ! discount) {
return
}
if (discount.percent_off) {
return `${discount.percent_off}%`;
} else {
return Vue.filter('currency')(
this.calculateAmountOff(discount.amount_off)
);
}
},
/**
* Calculate the amount off for the given discount amount.
*/
calculateAmountOff(amount) {
return amount / 100;
},
/**
* Get the formatted discount duration for the given discount.
*/
formattedDiscountDuration(discount) {
if ( ! discount) {
return;
}
switch (discount.duration) {
case 'forever':
return 'all future invoices';
case 'once':
return 'a single invoice';
case 'repeating':
return `all invoices during the next ${discount.duration_in_months} months`;
}
}
}
};

View File

@@ -0,0 +1,140 @@
/*
* This mixin is primarily used to share code for the "interval" selector
* on the registration and subscription screens, which is used to show
* all of the various subscription plans offered by the application.
*/
module.exports = {
data() {
return {
selectedPlan: null,
detailingPlan: null,
showingMonthlyPlans: true,
showingYearlyPlans: false
};
},
methods: {
/**
* Switch to showing monthly plans.
*/
showMonthlyPlans() {
this.showingMonthlyPlans = true;
this.showingYearlyPlans = false;
},
/**
* Switch to showing yearly plans.
*/
showYearlyPlans() {
this.showingMonthlyPlans = false;
this.showingYearlyPlans = true;
},
/**
* Show the plan details for the given plan.
*/
showPlanDetails(plan) {
this.detailingPlan = plan;
$('#modal-plan-details').modal('show');
}
},
computed: {
/**
* Get the active "interval" being displayed.
*/
activeInterval() {
return this.showingMonthlyPlans ? 'monthly' : 'yearly';
},
/**
* Get all of the plans for the active interval.
*/
plansForActiveInterval() {
return _.filter(this.plans, plan => {
return plan.active && (plan.price == 0 || plan.interval == this.activeInterval);
});
},
/**
* Get all of the paid plans.
*/
paidPlans() {
return _.filter(this.plans, plan => {
return plan.active && plan.price > 0;
});
},
/**
* Get all of the paid plans for the active interval.
*/
paidPlansForActiveInterval() {
return _.filter(this.plansForActiveInterval, plan => {
return plan.active && plan.price > 0;
});
},
/**
* Determine if both monthly and yearly plans are available.
*/
hasMonthlyAndYearlyPlans() {
return this.monthlyPlans.length > 0 && this.yearlyPlans.length > 0;
},
/**
* Determine if both monthly and yearly plans are available.
*/
hasMonthlyAndYearlyPaidPlans() {
return _.where(this.paidPlans, {interval: 'monthly'}).length > 0 &&
_.where(this.paidPlans, {interval: 'yearly'}).length > 0;
},
/**
* Determine if only yearly plans are available.
*/
onlyHasYearlyPlans() {
return this.monthlyPlans.length == 0 && this.yearlyPlans.length > 0;
},
/**
* Determine if both monthly and yearly plans are available.
*/
onlyHasYearlyPaidPlans() {
return _.where(this.paidPlans, {interval: 'monthly'}).length == 0 &&
_.where(this.paidPlans, {interval: 'yearly'}).length > 0;
},
/**
* Get all of the monthly plans.
*/
monthlyPlans() {
return _.filter(this.plans, plan => {
return plan.active && plan.interval == 'monthly';
});
},
/**
* Get all of the yearly plans.
*/
yearlyPlans() {
return _.filter(this.plans, plan => {
return plan.active && plan.interval == 'yearly';
});
}
}
};

View File

@@ -0,0 +1,138 @@
module.exports = {
/**
* The mixin's data.
*/
data() {
return {
plans: [],
selectedPlan: null,
invitation: null,
invalidInvitation: false
};
},
methods: {
/**
* Get the active plans for the application.
*/
getPlans() {
if ( ! Spark.cardUpFront) {
return;
}
axios.get('/spark/plans')
.then(response => {
var plans = response.data;
this.plans = _.where(plans, {type: "user"}).length > 0
? _.where(plans, {type: "user"})
: _.where(plans, {type: "team"});
this.selectAppropriateDefaultPlan();
});
},
/**
* Get the invitation specified in the query string.
*/
getInvitation() {
axios.get(`/invitations/${this.query.invitation}`)
.then(response => {
this.invitation = response.data;
})
.catch(response => {
this.invalidInvitation = true;
});
},
/**
* Select the appropriate default plan for registration.
*/
selectAppropriateDefaultPlan() {
if (this.query.plan) {
this.selectPlanById(this.query.plan) || this.selectPlanByName(this.query.plan);
} else if (this.query.invitation) {
this.selectFreePlan();
} else if (this.paidPlansForActiveInterval.length > 0) {
this.selectPlan(this.paidPlansForActiveInterval[0]);
} else {
this.selectFreePlan();
}
if (this.shouldShowYearlyPlans()) {
this.showYearlyPlans();
}
},
/**
* Select the free plan.
*/
selectFreePlan() {
const plan = _.find(this.plans, plan => plan.price === 0);
if (typeof plan !== 'undefined') {
this.selectPlan(plan);
}
},
/**
* Select the plan with the given id.
*/
selectPlanById(id) {
_.each(this.plans, plan => {
if (plan.id == id) {
this.selectPlan(plan);
}
});
return this.selectedPlan;
},
/**
* Select the plan with the given name.
*/
selectPlanByName(name) {
_.each(this.plans, plan => {
if (plan.name == name) {
this.selectPlan(plan);
}
});
return this.selectedPlan;
},
/**
* Determine if the given plan is selected.
*/
isSelected(plan) {
return this.selectedPlan && plan.id == this.selectedPlan.id;
},
/**
* Select the given plan.
*/
selectPlan(plan) {
this.selectedPlan = plan;
this.registerForm.plan = plan.id;
},
/**
* Determine if we should show the yearly plans.
*/
shouldShowYearlyPlans(){
return (this.monthlyPlans.length == 0 && this.yearlyPlans.length > 0) ||
this.selectedPlan.interval == 'yearly'
}
}
};

View File

@@ -0,0 +1,179 @@
/*
* This mixin is used by most of the subscription related screens to select plans
* and send subscription plan changes to the server. This contains helpers for
* the active subscription, trial information and other convenience helpers.
*/
module.exports = {
/**
* The mixin's data.
*/
data() {
return {
selectingPlan: null,
planForm: new SparkForm({})
}
},
methods: {
/**
* Update the subscription to the given plan.
*
* Used when updating or resuming the subscription plan.
*/
updateSubscription(plan) {
this.selectingPlan = plan;
this.planForm.errors.forget();
// Here we will send the request to the server to update the subscription plan and
// update the user and team once the request is complete. This method gets used
// for both updating subscriptions plus resuming any cancelled subscriptions.
axios.put(this.urlForPlanUpdate, {"plan": plan.id})
.then(() => {
Bus.$emit('updateUser');
Bus.$emit('updateTeam');
})
.catch(errors => {
if (errors.response.status == 422) {
this.planForm.errors.set(errors.response.data);
} else {
this.planForm.errors.set({plan: ["We were unable to update your subscription. Please contact customer support."]});
}
})
.finally(() => {
this.selectingPlan = null;
});
},
/**
* Determine if the given plan is selected.
*/
isActivePlan(plan) {
return this.activeSubscription &&
this.activeSubscription.provider_plan == plan.id;
}
},
computed: {
/**
* Get the active plan instance.
*/
activePlan() {
if (this.activeSubscription) {
return _.find(this.plans, (plan) => {
return plan.id == this.activeSubscription.provider_plan;
});
}
},
/**
* Determine if the active plan is a monthly plan.
*/
activePlanIsMonthly() {
return this.activePlan && this.activePlan.interval == 'monthly';
},
/**
* Get the active subscription instance.
*/
activeSubscription() {
if ( ! this.billable) {
return;
}
const subscription = _.find(
this.billable.subscriptions,
subscription => subscription.name == 'default'
);
if (typeof subscription !== 'undefined') {
return subscription;
}
},
/**
* Determine if the current subscription is active.
*/
subscriptionIsActive() {
return this.activeSubscription && ! this.activeSubscription.ends_at;
},
/**
* Determine if the billable entity is on a generic trial.
*/
onGenericTrial() {
return this.billable.trial_ends_at &&
moment.utc(this.billable.trial_ends_at).isAfter(moment.utc());
},
/**
* Determine if the current subscription is active.
*/
subscriptionIsOnTrial() {
if (this.onGenericTrial) {
return true;
}
return this.activeSubscription &&
this.activeSubscription.trial_ends_at &&
moment.utc().isBefore(moment.utc(this.activeSubscription.trial_ends_at));
},
/**
* Get the formatted trial ending date.
*/
trialEndsAt() {
if ( ! this.subscriptionIsOnTrial) {
return;
}
if (this.onGenericTrial) {
return moment.utc(this.billable.trial_ends_at)
.local().format('MMMM Do, YYYY');
}
return moment.utc(this.activeSubscription.trial_ends_at)
.local().format('MMMM Do, YYYY');
},
/**
* Determine if the current subscription is active.
*/
subscriptionIsOnGracePeriod() {
return this.activeSubscription &&
this.activeSubscription.ends_at &&
moment.utc().isBefore(moment.utc(this.activeSubscription.ends_at));
},
/**
* Determine if the billable entity has no active subscription.
*/
needsSubscription() {
return ! this.activeSubscription ||
(this.activeSubscription.ends_at &&
moment.utc().isAfter(moment.utc(this.activeSubscription.ends_at)));
},
/**
* Get the URL for the subscription plan update.
*/
urlForPlanUpdate() {
return this.billingUser
? '/settings/subscription'
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
}
}
};

View File

@@ -0,0 +1,91 @@
module.exports = {
pushStateSelector: null,
methods: {
/**
* Initialize push state handling for tabs.
*/
usePushStateForTabs(selector) {
this.pushStateSelector = selector;
this.registerTabClickHandler();
window.addEventListener('popstate', e => {
this.activateTabForCurrentHash();
});
if (window.location.hash) {
this.activateTabForCurrentHash();
} else {
this.activateFirstTab();
}
},
/**
* Register the click handler for all of the tabs.
*/
registerTabClickHandler() {
const self = this;
$(`${this.pushStateSelector} a[data-toggle="tab"]`).on('click', function(e) {
self.removeActiveClassFromTabs();
history.pushState(null, null, '#/' + $(this).attr('href').substring(1));
self.broadcastTabChange($(this).attr('href').substring(1));
});
},
/**
* Activate the tab for the current hash in the URL.
*/
activateTabForCurrentHash() {
var hash = window.location.hash.substring(2);
var parameters = hash.split('/');
hash = parameters.shift();
this.removeActiveClassFromTabs();
const tab = $(`${this.pushStateSelector} a[href="#${hash}"][data-toggle="tab"]`);
if (tab.length > 0) {
tab.tab('show');
}
this.broadcastTabChange(hash, parameters);
},
/**
* Activate the first tab in a list.
*/
activateFirstTab() {
const tab = $(`${this.pushStateSelector} a[data-toggle="tab"]`).first();
tab.tab('show');
this.broadcastTabChange(tab.attr('href').substring(1));
},
/**
* Remove the active class from the tabs.
*/
removeActiveClassFromTabs() {
$(`${this.pushStateSelector} li`).removeClass('active');
},
/**
* Broadcast that a tab change happened.
*/
broadcastTabChange(hash, parameters) {
Bus.$emit('sparkHashChanged', hash, parameters);
}
}
};

44
spark/resources/assets/js/mixins/vat.js vendored Normal file
View File

@@ -0,0 +1,44 @@
module.exports = {
methods: {
/**
* Determine if the given country collects European VAT.
*/
collectsVat(country) {
return Spark.collectsEuropeanVat ? _.contains([
'BE', 'BG', 'CZ', 'DK', 'DE',
'EE', 'IE', 'GR', 'ES', 'FR',
'HR', 'IT', 'CY', 'LV', 'LT',
'LU', 'HU', 'MT', 'NL', 'AT',
'PL', 'PT', 'RO', 'SI', 'SK',
'FI', 'SE', 'GB',
], country) : false;
},
/**
* Refresh the tax rate using the given form input.
*/
refreshTaxRate(form) {
axios.post('/tax-rate', JSON.parse(JSON.stringify(form)))
.then(response => {
this.taxRate = response.data.rate;
});
},
/**
* Get the tax amount for the selected plan.
*/
taxAmount(plan) {
return plan.price * (this.taxRate / 100);
},
/**
* Get the total plan price including the applicable tax.
*/
priceWithTax(plan) {
return plan.price + this.taxAmount(plan);
}
}
};