Initial Spark Install
This commit is contained in:
38
spark/resources/assets/js/mixins/braintree.js
vendored
Normal file
38
spark/resources/assets/js/mixins/braintree.js
vendored
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
96
spark/resources/assets/js/mixins/discounts.js
vendored
Normal file
96
spark/resources/assets/js/mixins/discounts.js
vendored
Normal 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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
140
spark/resources/assets/js/mixins/plans.js
vendored
Normal file
140
spark/resources/assets/js/mixins/plans.js
vendored
Normal 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';
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
138
spark/resources/assets/js/mixins/register.js
vendored
Normal file
138
spark/resources/assets/js/mixins/register.js
vendored
Normal 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'
|
||||
}
|
||||
}
|
||||
};
|
179
spark/resources/assets/js/mixins/subscriptions.js
vendored
Normal file
179
spark/resources/assets/js/mixins/subscriptions.js
vendored
Normal 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`;
|
||||
}
|
||||
}
|
||||
};
|
91
spark/resources/assets/js/mixins/tab-state.js
vendored
Normal file
91
spark/resources/assets/js/mixins/tab-state.js
vendored
Normal 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
44
spark/resources/assets/js/mixins/vat.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user