Initial Spark Install
This commit is contained in:
135
spark/resources/assets/js/auth/register-braintree.js
vendored
Normal file
135
spark/resources/assets/js/auth/register-braintree.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/braintree'),
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/register')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
|
||||
coupon: null,
|
||||
invalidCoupon: false,
|
||||
|
||||
registerForm: $.extend(true, new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
plan: '',
|
||||
team: '',
|
||||
team_slug: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
terms: false,
|
||||
coupon: null,
|
||||
invitation: null
|
||||
}), Spark.forms.register)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'registerForm.team': function (val, oldVal) {
|
||||
if (this.registerForm.team_slug == '' ||
|
||||
this.registerForm.team_slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.registerForm.team_slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
|
||||
this.query = URI(document.URL).query(true);
|
||||
|
||||
if (this.query.coupon) {
|
||||
this.getCoupon();
|
||||
|
||||
this.registerForm.coupon = this.query.coupon;
|
||||
}
|
||||
|
||||
if (this.query.invitation) {
|
||||
this.getInvitation();
|
||||
|
||||
this.registerForm.invitation = this.query.invitation;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.configureBraintree();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
configureBraintree() {
|
||||
if ( ! Spark.cardUpFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.braintree('braintree-container', response => {
|
||||
this.registerForm.braintree_type = response.type;
|
||||
this.registerForm.braintree_token = response.nonce;
|
||||
|
||||
this.register();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the coupon specified in the query string.
|
||||
*/
|
||||
getCoupon() {
|
||||
axios.get('/coupon/' + this.query.coupon)
|
||||
.then(response => {
|
||||
this.coupon = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
this.invalidCoupon = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to register with the application.
|
||||
*/
|
||||
register() {
|
||||
Spark.post('/register', this.registerForm)
|
||||
.then(response => {
|
||||
window.location = response.redirect;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the displayable discount for the coupon.
|
||||
*/
|
||||
discount() {
|
||||
if (this.coupon) {
|
||||
return Vue.filter('currency')(this.coupon.amount_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
249
spark/resources/assets/js/auth/register-stripe.js
vendored
Normal file
249
spark/resources/assets/js/auth/register-stripe.js
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/register'),
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/vat')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
|
||||
coupon: null,
|
||||
invalidCoupon: false,
|
||||
|
||||
country: null,
|
||||
taxRate: 0,
|
||||
|
||||
registerForm: $.extend(true, new SparkForm({
|
||||
stripe_token: '',
|
||||
plan: '',
|
||||
team: '',
|
||||
team_slug: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US',
|
||||
vat_id: '',
|
||||
terms: false,
|
||||
coupon: null,
|
||||
invitation: null
|
||||
}), Spark.forms.register),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: '',
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch for changes on the entire billing address.
|
||||
*/
|
||||
'currentBillingAddress': function (value) {
|
||||
if ( ! Spark.collectsEuropeanVat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshTaxRate(this.registerForm);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'registerForm.team': function (val, oldVal) {
|
||||
if (this.registerForm.team_slug == '' ||
|
||||
this.registerForm.team_slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.registerForm.team_slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.guessCountry();
|
||||
|
||||
this.query = URI(document.URL).query(true);
|
||||
|
||||
if (this.query.coupon) {
|
||||
this.getCoupon();
|
||||
|
||||
this.registerForm.coupon = this.query.coupon;
|
||||
}
|
||||
|
||||
if (this.query.invitation) {
|
||||
this.getInvitation();
|
||||
|
||||
this.registerForm.invitation = this.query.invitation;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
//
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Attempt to guess the user's country.
|
||||
*/
|
||||
guessCountry() {
|
||||
axios.get('/geocode/country')
|
||||
.then(response => {
|
||||
if (response.data != 'ZZ') {
|
||||
this.registerForm.country = response.data;
|
||||
}
|
||||
})
|
||||
.catch (response => {
|
||||
//
|
||||
})
|
||||
.finally(function () {
|
||||
this.refreshStatesAndProvinces();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the coupon specified in the query string.
|
||||
*/
|
||||
getCoupon() {
|
||||
axios.get('/coupon/' + this.query.coupon)
|
||||
.then(response => {
|
||||
this.coupon = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
this.invalidCoupon = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to register with the application.
|
||||
*/
|
||||
register() {
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
this.registerForm.busy = true;
|
||||
this.registerForm.errors.forget();
|
||||
|
||||
if ( ! Spark.cardUpFront || this.selectedPlan.price == 0) {
|
||||
return this.sendRegistration();
|
||||
}
|
||||
|
||||
Stripe.card.createToken(this.stripePayload(), (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [response.error.message]})
|
||||
this.registerForm.busy = false;
|
||||
} else {
|
||||
this.registerForm.stripe_token = response.id;
|
||||
this.sendRegistration();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Build the Stripe payload based on the form input.
|
||||
*/
|
||||
stripePayload() {
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
return {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.registerForm.address,
|
||||
address_line2: this.registerForm.address_line_2,
|
||||
address_city: this.registerForm.city,
|
||||
address_state: this.registerForm.state,
|
||||
address_zip: this.registerForm.zip,
|
||||
address_country: this.registerForm.country,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* After obtaining the Stripe token, send the registration to Spark.
|
||||
*/
|
||||
sendRegistration() {
|
||||
Spark.post('/register', this.registerForm)
|
||||
.then(response => {
|
||||
window.location = response.redirect;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine if the selected country collects European VAT.
|
||||
*/
|
||||
countryCollectsVat() {
|
||||
return this.collectsVat(this.registerForm.country);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the displayable discount for the coupon.
|
||||
*/
|
||||
discount() {
|
||||
if (this.coupon) {
|
||||
if (this.coupon.percent_off) {
|
||||
return this.coupon.percent_off + '%';
|
||||
} else {
|
||||
return Vue.filter('currency')(this.coupon.amount_off / 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current billing address from the register form.
|
||||
*
|
||||
* This used primarily for wathcing.
|
||||
*/
|
||||
currentBillingAddress() {
|
||||
return this.registerForm.address +
|
||||
this.registerForm.address_line_2 +
|
||||
this.registerForm.city +
|
||||
this.registerForm.state +
|
||||
this.registerForm.zip +
|
||||
this.registerForm.country +
|
||||
this.registerForm.vat_id;
|
||||
}
|
||||
}
|
||||
};
|
69
spark/resources/assets/js/filters.js
vendored
Normal file
69
spark/resources/assets/js/filters.js
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Format the given date.
|
||||
*/
|
||||
Vue.filter('date', value => {
|
||||
return moment.utc(value).local().format('MMMM Do, YYYY')
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given date as a timestamp.
|
||||
*/
|
||||
Vue.filter('datetime', value => {
|
||||
return moment.utc(value).local().format('MMMM Do, YYYY h:mm A');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given date into a relative time.
|
||||
*/
|
||||
Vue.filter('relative', value => {
|
||||
return moment.utc(value).local().locale('en-short').fromNow();
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Convert the first character to upper case.
|
||||
*
|
||||
* Source: https://github.com/vuejs/vue/blob/1.0/src/filters/index.js#L37
|
||||
*/
|
||||
Vue.filter('capitalize', value => {
|
||||
if (! value && value !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.toString().charAt(0).toUpperCase()
|
||||
+ value.slice(1);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given money value.
|
||||
*
|
||||
* Source: https://github.com/vuejs/vue/blob/1.0/src/filters/index.js#L70
|
||||
*/
|
||||
Vue.filter('currency', value => {
|
||||
value = parseFloat(value);
|
||||
|
||||
if (! isFinite(value) || (! value && value !== 0)){
|
||||
return '';
|
||||
}
|
||||
|
||||
var stringified = Math.abs(value).toFixed(2);
|
||||
|
||||
var _int = stringified.slice(0, -1 - 2);
|
||||
|
||||
var i = _int.length % 3;
|
||||
|
||||
var head = i > 0
|
||||
? (_int.slice(0, i) + (_int.length > 3 ? ',' : ''))
|
||||
: '';
|
||||
|
||||
var _float = stringified.slice(-1 - 2);
|
||||
|
||||
var sign = value < 0 ? '-' : '';
|
||||
|
||||
return sign + window.Spark.currencySymbol + head +
|
||||
_int.slice(i).replace(/(\d{3})(?=\d)/g, '$1,') +
|
||||
_float;
|
||||
});
|
23
spark/resources/assets/js/forms/bootstrap.js
vendored
Normal file
23
spark/resources/assets/js/forms/bootstrap.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Initialize the Spark form extension points.
|
||||
*/
|
||||
Spark.forms = {
|
||||
register: {},
|
||||
updateContactInformation: {},
|
||||
updateTeamMember: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the SparkForm helper class.
|
||||
*/
|
||||
require('./form');
|
||||
|
||||
/**
|
||||
* Define the SparkFormError collection class.
|
||||
*/
|
||||
require('./errors');
|
||||
|
||||
/**
|
||||
* Add additional HTTP / form helpers to the Spark object.
|
||||
*/
|
||||
$.extend(Spark, require('./http'));
|
71
spark/resources/assets/js/forms/errors.js
vendored
Normal file
71
spark/resources/assets/js/forms/errors.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Spark form error collection class.
|
||||
*/
|
||||
window.SparkFormErrors = function () {
|
||||
this.errors = {};
|
||||
|
||||
/**
|
||||
* Determine if the collection has any errors.
|
||||
*/
|
||||
this.hasErrors = function () {
|
||||
return ! _.isEmpty(this.errors);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the collection has errors for a given field.
|
||||
*/
|
||||
this.has = function (field) {
|
||||
return _.indexOf(_.keys(this.errors), field) > -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the raw errors for the collection.
|
||||
*/
|
||||
this.all = function () {
|
||||
return this.errors;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the errors for the collection in a flat array.
|
||||
*/
|
||||
this.flatten = function () {
|
||||
return _.flatten(_.toArray(this.errors));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the first error message for a given field.
|
||||
*/
|
||||
this.get = function (field) {
|
||||
if (this.has(field)) {
|
||||
return this.errors[field][0];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the raw errors for the collection.
|
||||
*/
|
||||
this.set = function (errors) {
|
||||
if (typeof errors === 'object') {
|
||||
this.errors = errors;
|
||||
} else {
|
||||
this.errors = {'form': ['Something went wrong. Please try again or contact customer support.']};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove errors from the collection.
|
||||
*/
|
||||
this.forget = function (field) {
|
||||
if (typeof field === 'undefined') {
|
||||
this.errors = {};
|
||||
} else {
|
||||
Vue.delete(this.errors, field);
|
||||
}
|
||||
};
|
||||
};
|
51
spark/resources/assets/js/forms/form.js
vendored
Normal file
51
spark/resources/assets/js/forms/form.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* SparkForm helper class. Used to set common properties on all forms.
|
||||
*/
|
||||
window.SparkForm = function (data) {
|
||||
var form = this;
|
||||
|
||||
$.extend(this, data);
|
||||
|
||||
/**
|
||||
* Create the form error helper instance.
|
||||
*/
|
||||
this.errors = new SparkFormErrors();
|
||||
|
||||
this.busy = false;
|
||||
this.successful = false;
|
||||
|
||||
/**
|
||||
* Start processing the form.
|
||||
*/
|
||||
this.startProcessing = function () {
|
||||
form.errors.forget();
|
||||
form.busy = true;
|
||||
form.successful = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finish processing the form.
|
||||
*/
|
||||
this.finishProcessing = function () {
|
||||
form.busy = false;
|
||||
form.successful = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the errors and other state for the form.
|
||||
*/
|
||||
this.resetStatus = function () {
|
||||
form.errors.forget();
|
||||
form.busy = false;
|
||||
form.successful = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the errors on the form.
|
||||
*/
|
||||
this.setErrors = function (errors) {
|
||||
form.busy = false;
|
||||
form.errors.set(errors);
|
||||
};
|
||||
};
|
56
spark/resources/assets/js/forms/http.js
vendored
Normal file
56
spark/resources/assets/js/forms/http.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Helper method for making POST HTTP requests.
|
||||
*/
|
||||
post(uri, form) {
|
||||
return Spark.sendForm('post', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making PUT HTTP requests.
|
||||
*/
|
||||
put(uri, form) {
|
||||
return Spark.sendForm('put', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making PATCH HTTP requests.
|
||||
*/
|
||||
patch(uri, form) {
|
||||
return Spark.sendForm('patch', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making DELETE HTTP requests.
|
||||
*/
|
||||
delete(uri, form) {
|
||||
return Spark.sendForm('delete', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send the form to the back-end server.
|
||||
*
|
||||
* This function will clear old errors, update "busy" status, etc.
|
||||
*/
|
||||
sendForm(method, uri, form) {
|
||||
return new Promise((resolve, reject) => {
|
||||
form.startProcessing();
|
||||
|
||||
axios[method](uri, JSON.parse(JSON.stringify(form)))
|
||||
.then(response => {
|
||||
form.finishProcessing();
|
||||
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(errors => {
|
||||
form.setErrors(errors.response.data.errors);
|
||||
|
||||
reject(errors.response.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
27
spark/resources/assets/js/interceptors.js
vendored
Normal file
27
spark/resources/assets/js/interceptors.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = (request, next) => {
|
||||
|
||||
if (Cookies.get('XSRF-TOKEN') !== undefined) {
|
||||
request.headers.set('X-XSRF-TOKEN', Cookies.get('XSRF-TOKEN'));
|
||||
}
|
||||
|
||||
request.headers.set('X-CSRF-TOKEN', Spark.csrfToken);
|
||||
|
||||
/**
|
||||
* Intercept the incoming responses.
|
||||
*
|
||||
* Handle any unexpected HTTP errors and pop up modals, etc.
|
||||
*/
|
||||
next(response => {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
Vue.http.get('/logout');
|
||||
$('#modal-session-expired').modal('show');
|
||||
break;
|
||||
|
||||
case 402:
|
||||
window.location = '/settings#/subscription';
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
};
|
65
spark/resources/assets/js/kiosk/add-discount.js
vendored
Normal file
65
spark/resources/assets/js/kiosk/add-discount.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
function kioskAddDiscountForm () {
|
||||
return {
|
||||
type: 'amount',
|
||||
value: null,
|
||||
duration: 'once',
|
||||
months: null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mixins: [require('./../mixins/discounts')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
loadingCurrentDiscount: false,
|
||||
currentDiscount: null,
|
||||
|
||||
discountingUser: null,
|
||||
form: new SparkForm(kioskAddDiscountForm())
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('addDiscount', function (user) {
|
||||
self.form = new SparkForm(kioskAddDiscountForm());
|
||||
|
||||
self.setUser(user);
|
||||
|
||||
$('#modal-add-discount').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Set the user receiving teh discount.
|
||||
*/
|
||||
setUser(user) {
|
||||
this.discountingUser = user;
|
||||
|
||||
this.getCurrentDiscountForUser(user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Apply the discount to the user.
|
||||
*/
|
||||
applyDiscount() {
|
||||
Spark.post('/spark/kiosk/users/discount/' + this.discountingUser.id, this.form)
|
||||
.then(() => {
|
||||
$('#modal-add-discount').modal('hide');
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
116
spark/resources/assets/js/kiosk/announcements.js
vendored
Normal file
116
spark/resources/assets/js/kiosk/announcements.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
var announcementsCreateForm = function () {
|
||||
return {
|
||||
body: '',
|
||||
action_text: '',
|
||||
action_url: ''
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
announcements: [],
|
||||
updatingAnnouncement: null,
|
||||
deletingAnnouncement: null,
|
||||
|
||||
createForm: new SparkForm(announcementsCreateForm()),
|
||||
updateForm: new SparkForm(announcementsCreateForm()),
|
||||
|
||||
deleteForm: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'announcements' && self.announcements.length === 0) {
|
||||
self.getAnnouncements();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the announcements.
|
||||
*/
|
||||
getAnnouncements() {
|
||||
axios.get('/spark/kiosk/announcements')
|
||||
.then(response => {
|
||||
this.announcements = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a new announcement.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/spark/kiosk/announcements', this.createForm)
|
||||
.then(() => {
|
||||
this.createForm = new SparkForm(announcementsCreateForm());
|
||||
|
||||
this.getAnnouncements();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Edit the given announcement.
|
||||
*/
|
||||
editAnnouncement(announcement) {
|
||||
this.updatingAnnouncement = announcement;
|
||||
|
||||
this.updateForm.icon = announcement.icon;
|
||||
this.updateForm.body = announcement.body;
|
||||
this.updateForm.action_text = announcement.action_text;
|
||||
this.updateForm.action_url = announcement.action_url;
|
||||
|
||||
$('#modal-update-announcement').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified announcement.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/spark/kiosk/announcements/' + this.updatingAnnouncement.id, this.updateForm)
|
||||
.then(() => {
|
||||
this.getAnnouncements();
|
||||
|
||||
$('#modal-update-announcement').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the approval dialog for deleting an announcement.
|
||||
*/
|
||||
approveAnnouncementDelete(announcement) {
|
||||
this.deletingAnnouncement = announcement;
|
||||
|
||||
$('#modal-delete-announcement').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the specified announcement.
|
||||
*/
|
||||
deleteAnnouncement() {
|
||||
Spark.delete('/spark/kiosk/announcements/' + this.deletingAnnouncement.id, this.deleteForm)
|
||||
.then(() => {
|
||||
this.getAnnouncements();
|
||||
|
||||
$('#modal-delete-announcement').modal('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
33
spark/resources/assets/js/kiosk/kiosk.js
vendored
Normal file
33
spark/resources/assets/js/kiosk/kiosk.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'users') {
|
||||
setTimeout(() => {
|
||||
$('#kiosk-users-search').focus();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
294
spark/resources/assets/js/kiosk/metrics.js
vendored
Normal file
294
spark/resources/assets/js/kiosk/metrics.js
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
monthlyRecurringRevenue: 0,
|
||||
yearlyRecurringRevenue: 0,
|
||||
totalVolume: 0,
|
||||
genericTrialUsers: 0,
|
||||
|
||||
indicators: [],
|
||||
lastMonthsIndicators: null,
|
||||
lastYearsIndicators: null,
|
||||
|
||||
plans: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'metrics' && self.yearlyRecurringRevenue === 0) {
|
||||
self.getRevenue();
|
||||
self.getPlans();
|
||||
self.getTrialUsers();
|
||||
self.getPerformanceIndicators();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the revenue information for the application.
|
||||
*/
|
||||
getRevenue() {
|
||||
axios.get('/spark/kiosk/performance-indicators/revenue')
|
||||
.then(response => {
|
||||
this.yearlyRecurringRevenue = response.data.yearlyRecurringRevenue;
|
||||
this.monthlyRecurringRevenue = response.data.monthlyRecurringRevenue;
|
||||
this.totalVolume = response.data.totalVolume;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the subscriber information for the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/kiosk/performance-indicators/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the number of users that are on a generic trial.
|
||||
*/
|
||||
getTrialUsers() {
|
||||
axios.get('/spark/kiosk/performance-indicators/trialing')
|
||||
.then(response => {
|
||||
this.genericTrialUsers = parseInt(response.data);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the performance indicators for the application.
|
||||
*/
|
||||
getPerformanceIndicators() {
|
||||
axios.get('/spark/kiosk/performance-indicators')
|
||||
.then(response => {
|
||||
this.indicators = response.data.indicators;
|
||||
this.lastMonthsIndicators = response.data.last_month;
|
||||
this.lastYearsIndicators = response.data.last_year;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
this.drawCharts();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the performance indicator charts.
|
||||
*/
|
||||
drawCharts() {
|
||||
this.drawMonthlyRecurringRevenueChart();
|
||||
this.drawYearlyRecurringRevenueChart();
|
||||
this.drawDailyVolumeChart();
|
||||
this.drawNewUsersChart();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the monthly recurring revenue chart.
|
||||
*/
|
||||
drawMonthlyRecurringRevenueChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'monthlyRecurringRevenueChart', 30, indicator => indicator.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the yearly recurring revenue chart.
|
||||
*/
|
||||
drawYearlyRecurringRevenueChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'yearlyRecurringRevenueChart', 30, indicator => indicator.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the daily volume chart.
|
||||
*/
|
||||
drawDailyVolumeChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'dailyVolumeChart', 14, indicator => indicator.daily_volume
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the daily new users chart.
|
||||
*/
|
||||
drawNewUsersChart() {
|
||||
return this.drawChart(
|
||||
'newUsersChart', 14, indicator => indicator.new_users
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw a chart with currency formatting on the Y-Axis.
|
||||
*/
|
||||
drawCurrencyChart(id, days, dataGatherer) {
|
||||
return this.drawChart(id, days, dataGatherer, value =>
|
||||
Vue.filter('currency')(value.value)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw a chart with the given parameters.
|
||||
*/
|
||||
drawChart(id, days, dataGatherer, scaleLabelFormatter) {
|
||||
var dataset = JSON.parse(JSON.stringify(this.baseChartDataSet));
|
||||
|
||||
dataset.data = _.map(_.last(this.indicators, days), dataGatherer);
|
||||
|
||||
// Here we will build out the dataset for the chart. This will contain the dates and data
|
||||
// points for the chart. Each chart on the Kiosk only gets one dataset so we only need
|
||||
// to add it a single element to this array here. But, charts could have more later.
|
||||
var data = {
|
||||
labels: _.last(this.availableChartDates, days),
|
||||
datasets: [dataset]
|
||||
};
|
||||
|
||||
var options = { responsive: true };
|
||||
|
||||
// If a scale label formatter was passed, we will hand that to this chart library to fill
|
||||
// out the Y-Axis labels. This is particularly useful when we want to format them as a
|
||||
// currency as we do on all of our revenue charts that we display on the Kiosk here.
|
||||
if (arguments.length === 4) {
|
||||
options.scaleLabel = scaleLabelFormatter;
|
||||
}
|
||||
|
||||
var chart = new Chart(document.getElementById(id).getContext('2d'), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the percent change between two numbers.
|
||||
*/
|
||||
percentChange(current, previous) {
|
||||
var change = Math.round(((current - previous) / previous) * 100);
|
||||
|
||||
return change > 0 ? '+' + change.toFixed(0) : change.toFixed(0);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Calculate the monthly change in monthly recurring revenue.
|
||||
*/
|
||||
monthlyChangeInMonthlyRecurringRevenue() {
|
||||
if ( ! this.lastMonthsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).monthly_recurring_revenue,
|
||||
this.lastMonthsIndicators.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the yearly change in monthly recurring revenue.
|
||||
*/
|
||||
yearlyChangeInMonthlyRecurringRevenue() {
|
||||
if ( ! this.lastYearsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).monthly_recurring_revenue,
|
||||
this.lastYearsIndicators.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the monthly change in yearly recurring revenue.
|
||||
*/
|
||||
monthlyChangeInYearlyRecurringRevenue() {
|
||||
if ( ! this.lastMonthsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).yearly_recurring_revenue,
|
||||
this.lastMonthsIndicators.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the yearly change in yearly recurring revenue.
|
||||
*/
|
||||
yearlyChangeInYearlyRecurringRevenue() {
|
||||
if ( ! this.lastYearsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
;
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).yearly_recurring_revenue,
|
||||
this.lastYearsIndicators.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the total number of users trialing.
|
||||
*/
|
||||
totalTrialUsers() {
|
||||
return this.genericTrialUsers + _.reduce(this.plans, (memo, plan) => {
|
||||
return memo + plan.trialing;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the available, formatted chart dates for the current indicators.
|
||||
*/
|
||||
availableChartDates() {
|
||||
return _.map(this.indicators, indicator => {
|
||||
return moment(indicator.created_at).format('M/D');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the base chart data set.
|
||||
*/
|
||||
baseChartDataSet() {
|
||||
return {
|
||||
label: "Dataset",
|
||||
fillColor: "rgba(151,187,205,0.2)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(151,187,205,1)",
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
148
spark/resources/assets/js/kiosk/profile.js
vendored
Normal file
148
spark/resources/assets/js/kiosk/profile.js
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
module.exports = {
|
||||
props: ['user', 'plans'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
profile: null,
|
||||
revenue: 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$parent.$on('showUserProfile', function(id) {
|
||||
self.getUserProfile(id);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Mousetrap.bind('esc', e => this.showSearch());
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the profile user.
|
||||
*/
|
||||
getUserProfile(id) {
|
||||
this.loading = true;
|
||||
|
||||
axios.get('/spark/kiosk/users/' + id + '/profile')
|
||||
.then(response => {
|
||||
this.profile = response.data.user;
|
||||
this.revenue = response.data.revenue;
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Impersonate the given user.
|
||||
*/
|
||||
impersonate(user) {
|
||||
window.location = '/spark/kiosk/users/impersonate/' + user.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the discount modal for the given user.
|
||||
*/
|
||||
addDiscount(user) {
|
||||
Bus.$emit('addDiscount', user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan the user is actively subscribed to.
|
||||
*/
|
||||
activePlan(billable) {
|
||||
if (this.activeSubscription(billable)) {
|
||||
var activeSubscription = this.activeSubscription(billable);
|
||||
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active, valid subscription for the user.
|
||||
*/
|
||||
activeSubscription(billable) {
|
||||
var subscription = this.subscription(billable);
|
||||
|
||||
if ( ! subscription ||
|
||||
(subscription.ends_at &&
|
||||
moment.utc().isAfter(moment.utc(subscription.ends_at)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subscription;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
subscription(billable) {
|
||||
if ( ! billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the customer URL on the billing provider's website.
|
||||
*/
|
||||
customerUrlOnBillingProvider(billable) {
|
||||
if (! billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.spark.usesStripe) {
|
||||
return 'https://dashboard.stripe.com/customers/' + billable.stripe_id;
|
||||
} else {
|
||||
var domain = Spark.env == 'production' ? '' : 'sandbox.';
|
||||
|
||||
return 'https://' + domain + 'braintreegateway.com/merchants/' +
|
||||
Spark.braintreeMerchantId +
|
||||
'/customers/' +
|
||||
billable.braintree_id;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results and hide the user profile.
|
||||
*/
|
||||
showSearch() {
|
||||
this.$parent.$emit('showSearch');
|
||||
|
||||
this.profile = null;
|
||||
}
|
||||
}
|
||||
};
|
123
spark/resources/assets/js/kiosk/users.js
vendored
Normal file
123
spark/resources/assets/js/kiosk/users.js
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
searchForm: new SparkForm({
|
||||
query: ''
|
||||
}),
|
||||
|
||||
searching: false,
|
||||
noSearchResults: false,
|
||||
searchResults: [],
|
||||
|
||||
showingUserProfile: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.$on('showSearch', function(){
|
||||
self.navigateToSearch();
|
||||
});
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash != 'users') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameters && parameters.length > 0) {
|
||||
self.loadProfile({ id: parameters[0] });
|
||||
} else {
|
||||
self.showSearch();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the available subscription plans.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Perform a search for the given query.
|
||||
*/
|
||||
search() {
|
||||
this.searching = true;
|
||||
this.noSearchResults = false;
|
||||
|
||||
axios.post('/spark/kiosk/users/search', this.searchForm)
|
||||
.then(response => {
|
||||
this.searchResults = response.data;
|
||||
this.noSearchResults = this.searchResults.length === 0;
|
||||
|
||||
this.searching = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results and update the browser history.
|
||||
*/
|
||||
navigateToSearch() {
|
||||
history.pushState(null, null, '#/users');
|
||||
|
||||
this.showSearch();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results.
|
||||
*/
|
||||
showSearch() {
|
||||
this.showingUserProfile = false;
|
||||
|
||||
Vue.nextTick(function () {
|
||||
$('#kiosk-users-search').focus();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the user profile for the given user.
|
||||
*/
|
||||
showUserProfile(user) {
|
||||
history.pushState(null, null, '#/users/' + user.id);
|
||||
|
||||
this.loadProfile(user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Load the user profile for the given user.
|
||||
*/
|
||||
loadProfile(user) {
|
||||
this.$emit('showUserProfile', user.id);
|
||||
|
||||
this.showingUserProfile = true;
|
||||
}
|
||||
}
|
||||
};
|
30
spark/resources/assets/js/mixin.js
vendored
Normal file
30
spark/resources/assets/js/mixin.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity.
|
||||
*/
|
||||
billable() {
|
||||
if (this.billableType) {
|
||||
return this.billableType == 'user' ? this.user : this.team;
|
||||
} else {
|
||||
return this.user;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current billable entity is a user.
|
||||
*/
|
||||
billingUser() {
|
||||
return this.billableType && this.billableType == 'user';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Access the global Spark object.
|
||||
*/
|
||||
spark() {
|
||||
return window.Spark;
|
||||
}
|
||||
}
|
||||
};
|
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);
|
||||
}
|
||||
}
|
||||
};
|
24
spark/resources/assets/js/navbar/navbar.js
vendored
Normal file
24
spark/resources/assets/js/navbar/navbar.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
props: [
|
||||
'user', 'teams', 'currentTeam',
|
||||
'hasUnreadNotifications', 'hasUnreadAnnouncements'
|
||||
],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the user's notifications.
|
||||
*/
|
||||
showNotifications() {
|
||||
Bus.$emit('showNotifications');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the customer support e-mail form.
|
||||
*/
|
||||
showSupportForm() {
|
||||
Bus.$emit('showSupportForm');
|
||||
}
|
||||
}
|
||||
};
|
80
spark/resources/assets/js/notifications/notifications.js
vendored
Normal file
80
spark/resources/assets/js/notifications/notifications.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
module.exports = {
|
||||
props: ['notifications', 'hasUnreadAnnouncements', 'loadingNotifications'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
showingNotifications: true,
|
||||
showingAnnouncements: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the user notifications.
|
||||
*/
|
||||
showNotifications() {
|
||||
this.showingNotifications = true;
|
||||
this.showingAnnouncements = false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the product announcements.
|
||||
*/
|
||||
showAnnouncements() {
|
||||
this.showingNotifications = false;
|
||||
this.showingAnnouncements = true;
|
||||
|
||||
this.updateLastReadAnnouncementsTimestamp();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the last read announcements timestamp.
|
||||
*/
|
||||
updateLastReadAnnouncementsTimestamp() {
|
||||
axios.put('/user/last-read-announcements-at')
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active notifications or announcements.
|
||||
*/
|
||||
activeNotifications() {
|
||||
if ( ! this.notifications) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.showingNotifications) {
|
||||
return this.notifications.notifications;
|
||||
} else {
|
||||
return this.notifications.announcements;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any notifications.
|
||||
*/
|
||||
hasNotifications() {
|
||||
return this.notifications && this.notifications.notifications.length > 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any announcements.
|
||||
*/
|
||||
hasAnnouncements() {
|
||||
return this.notifications && this.notifications.announcements.length > 0;
|
||||
}
|
||||
}
|
||||
};
|
52
spark/resources/assets/js/settings/api.js
vendored
Normal file
52
spark/resources/assets/js/settings/api.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
tokens: [],
|
||||
availableAbilities: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getTokens();
|
||||
this.getAvailableAbilities();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateTokens', function(){
|
||||
self.getTokens();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the current API tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/settings/api/tokens')
|
||||
.then(response => this.tokens = response.data);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the available token abilities.
|
||||
*/
|
||||
getAvailableAbilities() {
|
||||
axios.get('/settings/api/token/abilities')
|
||||
.then(response => this.availableAbilities = response.data);
|
||||
}
|
||||
}
|
||||
};
|
139
spark/resources/assets/js/settings/api/create-token.js
vendored
Normal file
139
spark/resources/assets/js/settings/api/create-token.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
module.exports = {
|
||||
props: ['availableAbilities'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
showingToken: null,
|
||||
allAbilitiesAssigned: false,
|
||||
|
||||
form: new SparkForm({
|
||||
name: '',
|
||||
abilities: []
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
copyCommandSupported() {
|
||||
return document.queryCommandSupported('copy');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the available abilities for changes.
|
||||
*/
|
||||
availableAbilities() {
|
||||
if (this.availableAbilities.length > 0) {
|
||||
this.assignDefaultAbilities();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Assign all of the default abilities.
|
||||
*/
|
||||
assignDefaultAbilities() {
|
||||
var defaults = _.filter(this.availableAbilities, a => a.default);
|
||||
|
||||
this.form.abilities = _.pluck(defaults, 'value');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Enable all the available abilities for the given token.
|
||||
*/
|
||||
assignAllAbilities() {
|
||||
this.allAbilitiesAssigned = true;
|
||||
|
||||
this.form.abilities = _.pluck(this.availableAbilities, 'value');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove all of the abilities from the token.
|
||||
*/
|
||||
removeAllAbilities() {
|
||||
this.allAbilitiesAssigned = false;
|
||||
|
||||
this.form.abilities = [];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the given ability in the list of assigned abilities.
|
||||
*/
|
||||
toggleAbility(ability) {
|
||||
if (this.abilityIsAssigned(ability)) {
|
||||
this.form.abilities = _.reject(this.form.abilities, a => a == ability);
|
||||
} else {
|
||||
this.form.abilities.push(ability);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the given ability has been assigned to the token.
|
||||
*/
|
||||
abilityIsAssigned(ability) {
|
||||
return _.contains(this.form.abilities, ability);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a new API token.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/settings/api/token', this.form)
|
||||
.then(response => {
|
||||
this.showToken(response.token);
|
||||
|
||||
this.resetForm();
|
||||
|
||||
this.$parent.$emit('updateTokens');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Display the token to the user.
|
||||
*/
|
||||
showToken(token) {
|
||||
this.showingToken = token;
|
||||
|
||||
$('#modal-show-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the token and copy to Clipboard.
|
||||
*/
|
||||
selectToken() {
|
||||
$('#api-token').select();
|
||||
|
||||
if (this.copyCommandSupported) {
|
||||
document.execCommand("copy");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reset the token form back to its default state.
|
||||
*/
|
||||
resetForm() {
|
||||
this.form.name = '';
|
||||
|
||||
this.assignDefaultAbilities();
|
||||
|
||||
this.allAbilitiesAssigned = false;
|
||||
}
|
||||
}
|
||||
};
|
103
spark/resources/assets/js/settings/api/tokens.js
vendored
Normal file
103
spark/resources/assets/js/settings/api/tokens.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
module.exports = {
|
||||
props: ['tokens', 'availableAbilities'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
updatingToken: null,
|
||||
deletingToken: null,
|
||||
|
||||
updateTokenForm: new SparkForm({
|
||||
name: '',
|
||||
abilities: []
|
||||
}),
|
||||
|
||||
deleteTokenForm: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the edit token modal.
|
||||
*/
|
||||
editToken(token) {
|
||||
this.updatingToken = token;
|
||||
|
||||
this.initializeUpdateFormWith(token);
|
||||
|
||||
$('#modal-update-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the edit form with the given token.
|
||||
*/
|
||||
initializeUpdateFormWith(token) {
|
||||
this.updateTokenForm.name = token.name;
|
||||
|
||||
this.updateTokenForm.abilities = token.metadata.abilities;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the token being edited.
|
||||
*/
|
||||
updateToken() {
|
||||
Spark.put(`/settings/api/token/${this.updatingToken.id}`, this.updateTokenForm)
|
||||
.then(response => {
|
||||
this.$parent.$emit('updateTokens');
|
||||
|
||||
$('#modal-update-token').modal('hide');
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the ability on the current token being edited.
|
||||
*/
|
||||
toggleAbility(ability) {
|
||||
if (this.abilityIsAssigned(ability)) {
|
||||
this.updateTokenForm.abilities = _.reject(
|
||||
this.updateTokenForm.abilities, a => a == ability
|
||||
);
|
||||
} else {
|
||||
this.updateTokenForm.abilities.push(ability);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the ability has been assigned to the token being edited.
|
||||
*/
|
||||
abilityIsAssigned(ability) {
|
||||
return _.contains(this.updateTokenForm.abilities, ability);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get user confirmation that the token should be deleted.
|
||||
*/
|
||||
approveTokenDelete(token) {
|
||||
this.deletingToken = token;
|
||||
|
||||
$('#modal-delete-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the specified token.
|
||||
*/
|
||||
deleteToken() {
|
||||
Spark.delete(`/settings/api/token/${this.deletingToken.id}`, this.deleteTokenForm)
|
||||
.then(() => {
|
||||
this.$parent.$emit('updateTokens');
|
||||
|
||||
$('#modal-delete-token').modal('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
47
spark/resources/assets/js/settings/invoices.js
vendored
Normal file
47
spark/resources/assets/js/settings/invoices.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invoices: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getInvoices();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the user's billing invoices
|
||||
*/
|
||||
getInvoices() {
|
||||
axios.get(this.urlForInvoices)
|
||||
.then(response => {
|
||||
this.invoices = _.filter(response.data, invoice => {
|
||||
return invoice.total != '$0.00';
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for retrieving the invoices.
|
||||
*/
|
||||
urlForInvoices() {
|
||||
return this.billingUser
|
||||
? '/settings/invoices'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/invoices`;
|
||||
}
|
||||
}
|
||||
};
|
15
spark/resources/assets/js/settings/invoices/invoice-list.js
vendored
Normal file
15
spark/resources/assets/js/settings/invoices/invoice-list.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'invoices', 'billableType'],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the URL for downloading a given invoice.
|
||||
*/
|
||||
downloadUrlFor(invoice) {
|
||||
return this.billingUser
|
||||
? `/settings/invoice/${invoice.id}`
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/invoice/${invoice.id}`;
|
||||
}
|
||||
}
|
||||
};
|
45
spark/resources/assets/js/settings/invoices/update-extra-billing-information.js
vendored
Normal file
45
spark/resources/assets/js/settings/invoices/update-extra-billing-information.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
information: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.information = this.billable.extra_billing_information;
|
||||
},
|
||||
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the extra billing information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the extra billing information method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/extra-billing-information'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/extra-billing-information`;
|
||||
}
|
||||
}
|
||||
};
|
77
spark/resources/assets/js/settings/payment-method-braintree.js
vendored
Normal file
77
spark/resources/assets/js/settings/payment-method-braintree.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/discounts')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The componetn's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
currentDiscount: null,
|
||||
loadingCurrentDiscount: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateDiscount', function(){
|
||||
self.getCurrentDiscountForBillable(self.billableType, self.billable);
|
||||
|
||||
return true;
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getCurrentDiscountForBillable(this.billableType, this.billable);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Calculate the amount off for the given discount amount.
|
||||
*/
|
||||
calculateAmountOff(amount) {
|
||||
return amount;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the formatted discount duration for the given discount.
|
||||
*/
|
||||
formattedDiscountDuration(discount) {
|
||||
if ( ! discount) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (discount.duration) {
|
||||
case 'forever':
|
||||
return 'for all future invoices';
|
||||
case 'once':
|
||||
return 'a single invoice';
|
||||
case 'repeating':
|
||||
if (discount.duration_in_months === 1) {
|
||||
return 'all invoices during the next billing cycle';
|
||||
} else {
|
||||
return `all invoices during the next ${discount.duration_in_months} billing cycles`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
43
spark/resources/assets/js/settings/payment-method-stripe.js
vendored
Normal file
43
spark/resources/assets/js/settings/payment-method-stripe.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/discounts')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The componetn's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
currentDiscount: null,
|
||||
loadingCurrentDiscount: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateDiscount', function(){
|
||||
self.getCurrentDiscountForBillable(self.billableType, self.billable);
|
||||
|
||||
return true;
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getCurrentDiscountForBillable(this.billableType, this.billable);
|
||||
},
|
||||
};
|
41
spark/resources/assets/js/settings/payment-method/redeem-coupon.js
vendored
Normal file
41
spark/resources/assets/js/settings/payment-method/redeem-coupon.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
coupon: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Redeem the given coupon code.
|
||||
*/
|
||||
redeem() {
|
||||
Spark.post(this.urlForRedemption, this.form)
|
||||
.then(() => {
|
||||
this.form.coupon = '';
|
||||
|
||||
this.$parent.$emit('updateDiscount');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for redeeming a coupon.
|
||||
*/
|
||||
urlForRedemption() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method/coupon'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method/coupon`;
|
||||
}
|
||||
}
|
||||
};
|
104
spark/resources/assets/js/settings/payment-method/update-payment-method-braintree.js
vendored
Normal file
104
spark/resources/assets/js/settings/payment-method/update-payment-method-braintree.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/braintree')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.braintree(
|
||||
'braintree-payment-method-container',
|
||||
this.braintreeCallback
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the entity's card information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
this.resetBraintree(
|
||||
'braintree-payment-method-container',
|
||||
this.braintreeCallback
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The Braintree payment method received callback.
|
||||
*/
|
||||
braintreeCallback(response) {
|
||||
this.form.braintree_type = response.type;
|
||||
this.form.braintree_token = response.nonce;
|
||||
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the payment method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the proper brand icon for the customer's credit card.
|
||||
*/
|
||||
cardIcon() {
|
||||
if (! this.billable.card_brand) {
|
||||
return 'fa-credit-card';
|
||||
}
|
||||
|
||||
switch (this.billable.card_brand) {
|
||||
case 'American Express':
|
||||
return 'fa-cc-amex';
|
||||
case 'Diners Club':
|
||||
return 'fa-cc-diners-club';
|
||||
case 'Discover':
|
||||
return 'fa-cc-discover';
|
||||
case 'JCB':
|
||||
return 'fa-cc-jcb';
|
||||
case 'MasterCard':
|
||||
return 'fa-cc-mastercard';
|
||||
case 'Visa':
|
||||
return 'fa-cc-visa';
|
||||
default:
|
||||
return 'fa-credit-card';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
183
spark/resources/assets/js/settings/payment-method/update-payment-method-stripe.js
vendored
Normal file
183
spark/resources/assets/js/settings/payment-method/update-payment-method-stripe.js
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
stripe_token: '',
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US'
|
||||
}),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.initializeBillingAddress();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the billing address form for the billable entity.
|
||||
*/
|
||||
initializeBillingAddress() {
|
||||
if (! Spark.collectsBillingAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.address = this.billable.billing_address;
|
||||
this.form.address_line_2 = this.billable.billing_address_line_2;
|
||||
this.form.city = this.billable.billing_city;
|
||||
this.form.state = this.billable.billing_state;
|
||||
this.form.zip = this.billable.billing_zip;
|
||||
this.form.country = this.billable.billing_country || 'US';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the billable's card information.
|
||||
*/
|
||||
update() {
|
||||
this.form.busy = true;
|
||||
this.form.errors.forget();
|
||||
this.form.successful = false;
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
const payload = {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.form.address,
|
||||
address_line2: this.form.address_line_2,
|
||||
address_city: this.form.city,
|
||||
address_state: this.form.state,
|
||||
address_zip: this.form.zip,
|
||||
address_country: this.form.country,
|
||||
};
|
||||
|
||||
// Once we have the Stripe payload we'll send it off to Stripe and obtain a token
|
||||
// which we will send to the server to update this payment method. If there is
|
||||
// an error we will display that back out to the user for their information.
|
||||
Stripe.card.createToken(payload, (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [
|
||||
response.error.message
|
||||
]});
|
||||
|
||||
this.form.busy = false;
|
||||
} else {
|
||||
this.sendUpdateToServer(response.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send the credit card update information to the server.
|
||||
*/
|
||||
sendUpdateToServer(token) {
|
||||
this.form.stripe_token = token;
|
||||
|
||||
Spark.put(this.urlForUpdate, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
this.cardForm.name = '';
|
||||
this.cardForm.number = '';
|
||||
this.cardForm.cvc = '';
|
||||
this.cardForm.month = '';
|
||||
this.cardForm.year = '';
|
||||
|
||||
if ( ! Spark.collectsBillingAddress) {
|
||||
this.form.zip = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity's "billable" name.
|
||||
*/
|
||||
billableName() {
|
||||
return this.billingUser ? this.user.name : this.team.owner.name;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for the payment method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the proper brand icon for the customer's credit card.
|
||||
*/
|
||||
cardIcon() {
|
||||
if (! this.billable.card_brand) {
|
||||
return 'fa-cc-stripe';
|
||||
}
|
||||
|
||||
switch (this.billable.card_brand) {
|
||||
case 'American Express':
|
||||
return 'fa-cc-amex';
|
||||
case 'Diners Club':
|
||||
return 'fa-cc-diners-club';
|
||||
case 'Discover':
|
||||
return 'fa-cc-discover';
|
||||
case 'JCB':
|
||||
return 'fa-cc-jcb';
|
||||
case 'MasterCard':
|
||||
return 'fa-cc-mastercard';
|
||||
case 'Visa':
|
||||
return 'fa-cc-visa';
|
||||
default:
|
||||
return 'fa-cc-stripe';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the placeholder for the billable entity's credit card.
|
||||
*/
|
||||
placeholder() {
|
||||
if (this.billable.card_last_four) {
|
||||
return `************${this.billable.card_last_four}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
42
spark/resources/assets/js/settings/payment-method/update-vat-id.js
vendored
Normal file
42
spark/resources/assets/js/settings/payment-method/update-vat-id.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({ vat_id: '' })
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.vat_id = this.billable.vat_id;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the customer's VAT ID.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the VAT ID update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method/vat-id'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method/vat-id`;
|
||||
}
|
||||
}
|
||||
}
|
3
spark/resources/assets/js/settings/profile.js
vendored
Normal file
3
spark/resources/assets/js/settings/profile.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user']
|
||||
};
|
37
spark/resources/assets/js/settings/profile/update-contact-information.js
vendored
Normal file
37
spark/resources/assets/js/settings/profile/update-contact-information.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: $.extend(true, new SparkForm({
|
||||
name: '',
|
||||
email: ''
|
||||
}), Spark.forms.updateContactInformation)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.name = this.user.name;
|
||||
this.form.email = this.user.email;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's contact information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/settings/contact', this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
67
spark/resources/assets/js/settings/profile/update-profile-photo.js
vendored
Normal file
67
spark/resources/assets/js/settings/profile/update-profile-photo.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's profile photo.
|
||||
*/
|
||||
update(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if ( ! this.$refs.photo.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// We need to gather a fresh FormData instance with the profile photo appended to
|
||||
// the data so we can POST it up to the server. This will allow us to do async
|
||||
// uploads of the profile photos. We will update the user after this action.
|
||||
axios.post('/settings/photo', this.gatherFormData())
|
||||
.then(
|
||||
() => {
|
||||
Bus.$emit('updateUser');
|
||||
|
||||
self.form.finishProcessing();
|
||||
},
|
||||
(error) => {
|
||||
self.form.setErrors(error.response.data.errors);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Gather the form data for the photo upload.
|
||||
*/
|
||||
gatherFormData() {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('photo', this.$refs.photo.files[0]);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Calculate the style attribute for the photo preview.
|
||||
*/
|
||||
previewStyle() {
|
||||
return `background-image: url(${this.user.photo_url})`;
|
||||
}
|
||||
}
|
||||
};
|
27
spark/resources/assets/js/settings/security.js
vendored
Normal file
27
spark/resources/assets/js/settings/security.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
twoFactorResetCode: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('receivedTwoFactorResetCode', function (code) {
|
||||
self.twoFactorResetCode = code;
|
||||
|
||||
$('#modal-show-two-factor-reset-code').modal('show');
|
||||
});
|
||||
}
|
||||
};
|
25
spark/resources/assets/js/settings/security/disable-two-factor-auth.js
vendored
Normal file
25
spark/resources/assets/js/settings/security/disable-two-factor-auth.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Disable two-factor authentication for the user.
|
||||
*/
|
||||
disable() {
|
||||
Spark.delete('/settings/two-factor-auth', this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
39
spark/resources/assets/js/settings/security/enable-two-factor-auth.js
vendored
Normal file
39
spark/resources/assets/js/settings/security/enable-two-factor-auth.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
country_code: '',
|
||||
phone: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.country_code = this.user.country_code;
|
||||
this.form.phone = this.user.phone;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Enable two-factor authentication for the user.
|
||||
*/
|
||||
enable() {
|
||||
Spark.post('/settings/two-factor-auth', this.form)
|
||||
.then(code => {
|
||||
this.$parent.$emit('receivedTwoFactorResetCode', code);
|
||||
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
24
spark/resources/assets/js/settings/security/update-password.js
vendored
Normal file
24
spark/resources/assets/js/settings/security/update-password.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/settings/password', this.form);
|
||||
}
|
||||
}
|
||||
};
|
28
spark/resources/assets/js/settings/settings.js
vendored
Normal file
28
spark/resources/assets/js/settings/settings.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
billableType: 'user',
|
||||
team: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
}
|
||||
};
|
50
spark/resources/assets/js/settings/subscription.js
vendored
Normal file
50
spark/resources/assets/js/settings/subscription.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
var self = this;
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.$on('showPlanDetails', function (plan) {
|
||||
self.showPlanDetails(plan);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the active plans for the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = this.billingUser
|
||||
? _.where(response.data, {type: "user"})
|
||||
: _.where(response.data, {type: "team"});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
48
spark/resources/assets/js/settings/subscription/cancel-subscription.js
vendored
Normal file
48
spark/resources/assets/js/settings/subscription/cancel-subscription.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Confirm the cancellation operation.
|
||||
*/
|
||||
confirmCancellation() {
|
||||
$('#modal-confirm-cancellation').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the current subscription.
|
||||
*/
|
||||
cancel() {
|
||||
Spark.delete(this.urlForCancellation, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-confirm-cancellation').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the subscription cancellation.
|
||||
*/
|
||||
urlForCancellation() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
}
|
||||
}
|
||||
};
|
41
spark/resources/assets/js/settings/subscription/resume-subscription.js
vendored
Normal file
41
spark/resources/assets/js/settings/subscription/resume-subscription.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
if (this.onlyHasYearlyPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan price with the applicable VAT.
|
||||
*/
|
||||
priceWithTax(plan) {
|
||||
return plan.price + (plan.price * (this.billable.tax_rate / 100));
|
||||
}
|
||||
}
|
||||
};
|
96
spark/resources/assets/js/settings/subscription/subscribe-braintree.js
vendored
Normal file
96
spark/resources/assets/js/settings/subscription/subscribe-braintree.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/braintree'),
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
plan: '',
|
||||
coupon: null
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
// If only yearly subscription plans are available, we will select that interval so that we
|
||||
// can show the plans. Then we'll select the first available paid plan from the list and
|
||||
// start the form in a good default spot. The user may then select another plan later.
|
||||
if (this.onlyHasYearlyPaidPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
|
||||
// Next, we will configure the braintree container element on the page and handle the nonce
|
||||
// received callback. We'll then set the nonce and fire off the subscribe method so this
|
||||
// nonce can be used to create the subscription for the billable entity being managed.
|
||||
this.braintree('braintree-subscribe-container', response => {
|
||||
this.form.braintree_type = response.type;
|
||||
this.form.braintree_token = response.nonce;
|
||||
|
||||
this.subscribe();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Mark the given plan as selected.
|
||||
*/
|
||||
selectPlan(plan) {
|
||||
this.selectedPlan = plan;
|
||||
|
||||
this.form.plan = this.selectedPlan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to the specified plan.
|
||||
*/
|
||||
subscribe() {
|
||||
Spark.post(this.urlForNewSubscription, this.form)
|
||||
.then(response => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for subscribing to a plan.
|
||||
*/
|
||||
urlForNewSubscription() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
}
|
||||
}
|
||||
};
|
216
spark/resources/assets/js/settings/subscription/subscribe-stripe.js
vendored
Normal file
216
spark/resources/assets/js/settings/subscription/subscribe-stripe.js
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions'),
|
||||
require('./../../mixins/vat')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
taxRate: 0,
|
||||
|
||||
form: new SparkForm({
|
||||
stripe_token: '',
|
||||
plan: '',
|
||||
coupon: null,
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US',
|
||||
vat_id: ''
|
||||
}),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: '',
|
||||
zip: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch for changes on the entire billing address.
|
||||
*/
|
||||
'currentBillingAddress': function (value) {
|
||||
if ( ! Spark.collectsEuropeanVat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshTaxRate(this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.initializeBillingAddress();
|
||||
|
||||
if (this.onlyHasYearlyPaidPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the billing address form for the billable entity.
|
||||
*/
|
||||
initializeBillingAddress() {
|
||||
this.form.address = this.billable.billing_address;
|
||||
this.form.address_line_2 = this.billable.billing_address_line_2;
|
||||
this.form.city = this.billable.billing_city;
|
||||
this.form.state = this.billable.billing_state;
|
||||
this.form.zip = this.billable.billing_zip;
|
||||
this.form.country = this.billable.billing_country || 'US';
|
||||
this.form.vat_id = this.billable.vat_id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Mark the given plan as selected.
|
||||
*/
|
||||
selectPlan(plan) {
|
||||
this.selectedPlan = plan;
|
||||
|
||||
this.form.plan = this.selectedPlan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to the specified plan.
|
||||
*/
|
||||
subscribe() {
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
const payload = {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.form.address,
|
||||
address_line2: this.form.address_line_2,
|
||||
address_city: this.form.city,
|
||||
address_state: this.form.state,
|
||||
address_zip: this.form.zip,
|
||||
address_country: this.form.country
|
||||
};
|
||||
|
||||
// Next, we will send the payload to Stripe and handle the response. If we have a
|
||||
// valid token we can send that to the server and use the token to create this
|
||||
// subscription on the back-end. Otherwise, we will show the error messages.
|
||||
Stripe.card.createToken(payload, (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [
|
||||
response.error.message
|
||||
]})
|
||||
|
||||
this.form.busy = false;
|
||||
} else {
|
||||
this.createSubscription(response.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* After obtaining the Stripe token, create subscription on the Spark server.
|
||||
*/
|
||||
createSubscription(token) {
|
||||
this.form.stripe_token = token;
|
||||
|
||||
Spark.post(this.urlForNewSubscription, this.form)
|
||||
.then(response => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has subscribed to the given plan before.
|
||||
*/
|
||||
hasSubscribed(plan) {
|
||||
return !!_.where(this.billable.subscriptions, {provider_plan: plan.id}).length
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity's "billable" name.
|
||||
*/
|
||||
billableName() {
|
||||
return this.billingUser ? this.user.name : this.team.owner.name;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the selected country collects European VAT.
|
||||
*/
|
||||
countryCollectsVat() {
|
||||
return this.collectsVat(this.form.country);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for subscribing to a plan.
|
||||
*/
|
||||
urlForNewSubscription() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current billing address from the subscribe form.
|
||||
*
|
||||
* This used primarily for wathcing.
|
||||
*/
|
||||
currentBillingAddress() {
|
||||
return this.form.address +
|
||||
this.form.address_line_2 +
|
||||
this.form.city +
|
||||
this.form.state +
|
||||
this.form.zip +
|
||||
this.form.country +
|
||||
this.form.vat_id;
|
||||
}
|
||||
}
|
||||
};
|
92
spark/resources/assets/js/settings/subscription/update-subscription.js
vendored
Normal file
92
spark/resources/assets/js/settings/subscription/update-subscription.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
confirmingPlan: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.selectActivePlanInterval();
|
||||
|
||||
// We need to watch the activePlan computed property for changes so we can select
|
||||
// the proper active plan on the plan interval button group. So, we will watch
|
||||
// this property and fire off a method anytime it changes so it can sync up.
|
||||
this.$watch('activePlan', value => {
|
||||
this.selectActivePlanInterval();
|
||||
});
|
||||
|
||||
if (this.onlyHasYearlyPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Confirm the plan update with the user.
|
||||
*/
|
||||
confirmPlanUpdate(plan) {
|
||||
this.confirmingPlan = plan;
|
||||
|
||||
$('#modal-confirm-plan-update').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Approve the plan update.
|
||||
*/
|
||||
approvePlanUpdate() {
|
||||
$('#modal-confirm-plan-update').modal('hide');
|
||||
|
||||
this.updateSubscription(this.confirmingPlan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the active plan interval.
|
||||
*/
|
||||
selectActivePlanInterval() {
|
||||
if (this.activePlanIsMonthly || this.yearlyPlans.length == 0) {
|
||||
this.showMonthlyPlans();
|
||||
} else {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan price with the applicable VAT.
|
||||
*/
|
||||
priceWithTax(plan) {
|
||||
return plan.price + (plan.price * (this.billable.tax_rate / 100));
|
||||
}
|
||||
}
|
||||
};
|
3
spark/resources/assets/js/settings/teams.js
vendored
Normal file
3
spark/resources/assets/js/settings/teams.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
};
|
151
spark/resources/assets/js/settings/teams/create-team.js
vendored
Normal file
151
spark/resources/assets/js/settings/teams/create-team.js
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
form: new SparkForm({
|
||||
name: '',
|
||||
slug: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
activeSubscription() {
|
||||
if ( ! this.$parent.billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
this.$parent.billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active plan instance.
|
||||
*/
|
||||
activePlan() {
|
||||
if (this.activeSubscription) {
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == this.activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's a limit for the number of teams.
|
||||
*/
|
||||
hasTeamLimit() {
|
||||
if (! this.activePlan) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !! this.activePlan.attributes.teams;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the remaining teams in the active plan.
|
||||
*/
|
||||
remainingTeams() {
|
||||
var ownedTeams = _.filter(this.$parent.teams, {owner_id: this.$parent.billable.id});
|
||||
|
||||
return this.activePlan
|
||||
? this.activePlan.attributes.teams - ownedTeams.length
|
||||
: 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user can create more teams.
|
||||
*/
|
||||
canCreateMoreTeams() {
|
||||
if (! this.hasTeamLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.remainingTeams > 0;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
},
|
||||
|
||||
|
||||
events: {
|
||||
/**
|
||||
* Handle the "activatedTab" event.
|
||||
*/
|
||||
activatedTab(tab) {
|
||||
if (tab === Spark.pluralTeamString) {
|
||||
Vue.nextTick(() => {
|
||||
$('#create-team-name').focus();
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'form.name': function (val, oldVal) {
|
||||
if (this.form.slug == '' ||
|
||||
this.form.slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.form.slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Create a new team.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/settings/'+Spark.pluralTeamString, this.form)
|
||||
.then(() => {
|
||||
this.form.name = '';
|
||||
this.form.slug = '';
|
||||
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all the plans defined in the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
85
spark/resources/assets/js/settings/teams/current-teams.js
vendored
Normal file
85
spark/resources/assets/js/settings/teams/current-teams.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
leavingTeam: null,
|
||||
deletingTeam: null,
|
||||
|
||||
leaveTeamForm: new SparkForm({}),
|
||||
deleteTeamForm: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Approve leaving the given team.
|
||||
*/
|
||||
approveLeavingTeam(team) {
|
||||
this.leavingTeam = team;
|
||||
|
||||
$('#modal-leave-team').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Leave the given team.
|
||||
*/
|
||||
leaveTeam() {
|
||||
Spark.delete(this.urlForLeaving, this.leaveTeamForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
$('#modal-leave-team').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Approve the deletion of the given team.
|
||||
*/
|
||||
approveTeamDelete(team) {
|
||||
this.deletingTeam = team;
|
||||
|
||||
$('#modal-delete-team').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the given team.
|
||||
*/
|
||||
deleteTeam() {
|
||||
Spark.delete(`/settings/${Spark.pluralTeamString}/${this.deletingTeam.id}`, this.deleteTeamForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
$('#modal-delete-team').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for leaving a team.
|
||||
*/
|
||||
urlForLeaving() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.leavingTeam.id}/members/${this.user.id}`;
|
||||
}
|
||||
}
|
||||
};
|
16
spark/resources/assets/js/settings/teams/mailed-invitations.js
vendored
Normal file
16
spark/resources/assets/js/settings/teams/mailed-invitations.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
props: ['team', 'invitations'],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Cancel the sent invitation.
|
||||
*/
|
||||
cancel(invitation) {
|
||||
axios.delete(`/settings/invitations/${invitation.id}`)
|
||||
.then(() => {
|
||||
this.$parent.$emit('updateInvitations');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
67
spark/resources/assets/js/settings/teams/pending-invitations.js
vendored
Normal file
67
spark/resources/assets/js/settings/teams/pending-invitations.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invitations: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPendingInvitations();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the pending invitations for the user.
|
||||
*/
|
||||
getPendingInvitations() {
|
||||
axios.get('/settings/invitations/pending')
|
||||
.then(response => {
|
||||
this.invitations = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Accept the given invitation.
|
||||
*/
|
||||
accept(invitation) {
|
||||
axios.post(`/settings/invitations/${invitation.id}/accept`)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
this.getPendingInvitations();
|
||||
});
|
||||
|
||||
this.removeInvitation(invitation);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reject the given invitation.
|
||||
*/
|
||||
reject(invitation) {
|
||||
axios.post(`/settings/invitations/${invitation.id}/reject`)
|
||||
.then(() => {
|
||||
this.getPendingInvitations();
|
||||
});
|
||||
|
||||
this.removeInvitation(invitation);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove the given invitation from the list.
|
||||
*/
|
||||
removeInvitation(invitation) {
|
||||
this.invitations = _.reject(this.invitations, i => i.id === invitation.id);
|
||||
}
|
||||
}
|
||||
};
|
116
spark/resources/assets/js/settings/teams/send-invitation.js
vendored
Normal file
116
spark/resources/assets/js/settings/teams/send-invitation.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
form: new SparkForm({
|
||||
email: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active plan instance.
|
||||
*/
|
||||
activePlan() {
|
||||
if (this.activeSubscription) {
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == this.activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's a limit for the number of team members.
|
||||
*/
|
||||
hasTeamMembersLimit() {
|
||||
if (! this.activePlan) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !! this.activePlan.attributes.teamMembers;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the remaining team members in the active plan.
|
||||
*/
|
||||
remainingTeamMembers() {
|
||||
return this.activePlan
|
||||
? this.activePlan.attributes.teamMembers - this.$parent.team.users.length
|
||||
: 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user can invite more team members.
|
||||
*/
|
||||
canInviteMoreTeamMembers() {
|
||||
if (! this.hasTeamMembersLimit) {
|
||||
return true;
|
||||
}
|
||||
return this.remainingTeamMembers > 0;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Send a team invitation.
|
||||
*/
|
||||
send() {
|
||||
Spark.post(`/settings/${Spark.pluralTeamString}/${this.team.id}/invitations`, this.form)
|
||||
.then(() => {
|
||||
this.form.email = '';
|
||||
|
||||
this.$parent.$emit('updateInvitations');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all the plans defined in the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
144
spark/resources/assets/js/settings/teams/team-members.js
vendored
Normal file
144
spark/resources/assets/js/settings/teams/team-members.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
roles: [],
|
||||
|
||||
updatingTeamMember: null,
|
||||
deletingTeamMember: null,
|
||||
|
||||
updateTeamMemberForm: $.extend(true, new SparkForm({
|
||||
role: ''
|
||||
}), Spark.forms.updateTeamMember),
|
||||
|
||||
deleteTeamMemberForm: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getRoles();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the available team member roles.
|
||||
*/
|
||||
getRoles() {
|
||||
axios.get(`/settings/${Spark.pluralTeamString}/roles`)
|
||||
.then(response => {
|
||||
this.roles = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Edit the given team member.
|
||||
*/
|
||||
editTeamMember(member) {
|
||||
this.updatingTeamMember = member;
|
||||
this.updateTeamMemberForm.role = member.pivot.role;
|
||||
|
||||
$('#modal-update-team-member').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the team member.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdating, this.updateTeamMemberForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-update-team-member').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Display the approval modal for the deletion of a team member.
|
||||
*/
|
||||
approveTeamMemberDelete(member) {
|
||||
this.deletingTeamMember = member;
|
||||
|
||||
$('#modal-delete-member').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the given team member.
|
||||
*/
|
||||
deleteMember() {
|
||||
Spark.delete(this.urlForDeleting, this.deleteTeamMemberForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-delete-member').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current user can edit a team member.
|
||||
*/
|
||||
canEditTeamMember(member) {
|
||||
return this.user.id === this.team.owner_id && this.user.id !== member.id
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current user can delete a team member.
|
||||
*/
|
||||
canDeleteTeamMember(member) {
|
||||
return this.user.id === this.team.owner_id && this.user.id !== member.id
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the displayable role for the given team member.
|
||||
*/
|
||||
teamMemberRole(member) {
|
||||
if (this.roles.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (member.pivot.role == 'owner') {
|
||||
return 'Owner';
|
||||
}
|
||||
|
||||
const role = _.find(this.roles, role => role.value == member.pivot.role);
|
||||
|
||||
if (typeof role !== 'undefined') {
|
||||
return role.text;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for updating a team member.
|
||||
*/
|
||||
urlForUpdating: function () {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/members/${this.updatingTeamMember.id}`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for deleting a team member.
|
||||
*/
|
||||
urlForDeleting() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/members/${this.deletingTeamMember.id}`;
|
||||
}
|
||||
}
|
||||
};
|
39
spark/resources/assets/js/settings/teams/team-membership.js
vendored
Normal file
39
spark/resources/assets/js/settings/teams/team-membership.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invitations: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getInvitations();
|
||||
|
||||
this.$on('updateInvitations', function () {
|
||||
self.getInvitations();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the invitations for the team.
|
||||
*/
|
||||
getInvitations() {
|
||||
axios.get(`/settings/${Spark.pluralTeamString}/${this.team.id}/invitations`)
|
||||
.then(response => {
|
||||
this.invitations = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
3
spark/resources/assets/js/settings/teams/team-profile.js
vendored
Normal file
3
spark/resources/assets/js/settings/teams/team-profile.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team']
|
||||
};
|
55
spark/resources/assets/js/settings/teams/team-settings.js
vendored
Normal file
55
spark/resources/assets/js/settings/teams/team-settings.js
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teamId'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
billableType: 'team',
|
||||
team: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getTeam();
|
||||
|
||||
Bus.$on('updateTeam', function () {
|
||||
self.getTeam();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the team being managed.
|
||||
*/
|
||||
getTeam() {
|
||||
axios.get(`/${Spark.pluralTeamString}/${this.teamId}`)
|
||||
.then(response => {
|
||||
this.team = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
36
spark/resources/assets/js/settings/teams/update-team-name.js
vendored
Normal file
36
spark/resources/assets/js/settings/teams/update-team-name.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
name: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.name = this.team.name;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the team name.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(`/settings/${Spark.pluralTeamString}/${this.team.id}/name`, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
Bus.$emit('updateTeams');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
76
spark/resources/assets/js/settings/teams/update-team-photo.js
vendored
Normal file
76
spark/resources/assets/js/settings/teams/update-team-photo.js
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the team's photo.
|
||||
*/
|
||||
update(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if ( ! this.$refs.photo.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// We need to gather a fresh FormData instance with the profile photo appended to
|
||||
// the data so we can POST it up to the server. This will allow us to do async
|
||||
// uploads of the profile photos. We will update the user after this action.
|
||||
axios.post(this.urlForUpdate, this.gatherFormData())
|
||||
.then(
|
||||
() => {
|
||||
Bus.$emit('updateTeam');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
self.form.finishProcessing();
|
||||
},
|
||||
(error) => {
|
||||
self.form.setErrors(error.response.data.errors);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Gather the form data for the photo upload.
|
||||
*/
|
||||
gatherFormData() {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('photo', this.$refs.photo.files[0]);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for updating the team photo.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/photo`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the style attribute for the photo preview.
|
||||
*/
|
||||
previewStyle() {
|
||||
return `background-image: url(${this.team.photo_url})`;
|
||||
}
|
||||
}
|
||||
};
|
80
spark/resources/assets/js/spark-bootstrap.js
vendored
Normal file
80
spark/resources/assets/js/spark-bootstrap.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Load various JavaScript modules that assist Spark.
|
||||
*/
|
||||
window.URI = require('urijs');
|
||||
window.axios = require('axios');
|
||||
window._ = require('underscore');
|
||||
window.moment = require('moment');
|
||||
window.Promise = require('promise');
|
||||
window.Cookies = require('js-cookie');
|
||||
|
||||
/*
|
||||
* Define Moment locales
|
||||
*/
|
||||
window.moment.defineLocale('en-short', {
|
||||
parentLocale: 'en',
|
||||
relativeTime : {
|
||||
future: "in %s",
|
||||
past: "%s",
|
||||
s: "1s",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
M: "1 month ago",
|
||||
MM: "%d months ago",
|
||||
y: "1y",
|
||||
yy: "%dy"
|
||||
}
|
||||
});
|
||||
window.moment.locale('en');
|
||||
|
||||
/*
|
||||
* Load jQuery and Bootstrap jQuery, used for front-end interaction.
|
||||
*/
|
||||
if (window.$ === undefined || window.jQuery === undefined) {
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
}
|
||||
|
||||
require('bootstrap/dist/js/npm');
|
||||
|
||||
/**
|
||||
* Load Vue if this application is using Vue as its framework.
|
||||
*/
|
||||
if ($('#spark-app').length > 0) {
|
||||
require('vue-bootstrap');
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
window.axios.defaults.headers.common = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': Spark.csrfToken
|
||||
};
|
||||
|
||||
/**
|
||||
* Intercept the incoming responses.
|
||||
*
|
||||
* Handle any unexpected HTTP errors and pop up modals, etc.
|
||||
*/
|
||||
window.axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
window.axios.get('/logout');
|
||||
$('#modal-session-expired').modal('show');
|
||||
break;
|
||||
|
||||
case 402:
|
||||
window.location = '/settings#/subscription';
|
||||
break;
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
266
spark/resources/assets/js/spark.js
vendored
Normal file
266
spark/resources/assets/js/spark.js
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Export the root Spark application.
|
||||
*/
|
||||
module.exports = {
|
||||
el: '#spark-app',
|
||||
|
||||
|
||||
/**
|
||||
* Holds the timestamp for the last time we updated the API token.
|
||||
*/
|
||||
lastRefreshedApiTokenAt: null,
|
||||
|
||||
|
||||
/**
|
||||
* The application's data.
|
||||
*/
|
||||
data: {
|
||||
user: Spark.state.user,
|
||||
teams: Spark.state.teams,
|
||||
currentTeam: Spark.state.currentTeam,
|
||||
|
||||
loadingNotifications: false,
|
||||
notifications: null,
|
||||
|
||||
supportForm: new SparkForm({
|
||||
from: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
if (Spark.userId) {
|
||||
this.loadDataForAuthenticatedUser();
|
||||
}
|
||||
|
||||
if (Spark.userId && Spark.usesApi) {
|
||||
this.refreshApiTokenEveryFewMinutes();
|
||||
}
|
||||
|
||||
Bus.$on('updateUser', function () {
|
||||
self.getUser();
|
||||
});
|
||||
|
||||
Bus.$on('updateUserData', function() {
|
||||
self.loadDataForAuthenticatedUser();
|
||||
});
|
||||
|
||||
Bus.$on('updateTeams', function () {
|
||||
self.getTeams();
|
||||
});
|
||||
|
||||
Bus.$on('showNotifications', function () {
|
||||
$('#modal-notifications').modal('show');
|
||||
|
||||
self.markNotificationsAsRead();
|
||||
});
|
||||
|
||||
Bus.$on('showSupportForm', function () {
|
||||
if (self.user) {
|
||||
self.supportForm.from = self.user.email;
|
||||
}
|
||||
|
||||
$('#modal-support').modal('show');
|
||||
|
||||
setTimeout(() => {
|
||||
$('#support-subject').focus();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the application.
|
||||
*/
|
||||
mounted() {
|
||||
this.whenReady();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Finish bootstrapping the application.
|
||||
*/
|
||||
whenReady() {
|
||||
//
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Load the data for an authenticated user.
|
||||
*/
|
||||
loadDataForAuthenticatedUser() {
|
||||
this.getNotifications();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the current API token every few minutes.
|
||||
*/
|
||||
refreshApiTokenEveryFewMinutes() {
|
||||
this.lastRefreshedApiTokenAt = moment();
|
||||
|
||||
setInterval(() => {
|
||||
this.refreshApiToken();
|
||||
}, 240000);
|
||||
|
||||
setInterval(() => {
|
||||
if (moment().diff(this.lastRefreshedApiTokenAt, 'minutes') >= 5) {
|
||||
this.refreshApiToken();
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the current API token.
|
||||
*/
|
||||
refreshApiToken() {
|
||||
this.lastRefreshedApiTokenAt = moment();
|
||||
|
||||
axios.put('/spark/token');
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Get the current user of the application.
|
||||
*/
|
||||
getUser() {
|
||||
axios.get('/user/current')
|
||||
.then(response => {
|
||||
this.user = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current team list.
|
||||
*/
|
||||
getTeams() {
|
||||
axios.get('/'+Spark.pluralTeamString)
|
||||
.then(response => {
|
||||
this.teams = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current team.
|
||||
*/
|
||||
getCurrentTeam() {
|
||||
axios.get(`/${Spark.pluralTeamString}/current`)
|
||||
.then(response => {
|
||||
this.currentTeam = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
//
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the application notifications.
|
||||
*/
|
||||
getNotifications() {
|
||||
this.loadingNotifications = true;
|
||||
|
||||
axios.get('/notifications/recent')
|
||||
.then(response => {
|
||||
this.notifications = response.data;
|
||||
|
||||
this.loadingNotifications = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Mark the current notifications as read.
|
||||
*/
|
||||
markNotificationsAsRead() {
|
||||
if ( ! this.hasUnreadNotifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.put('/notifications/read', {
|
||||
notifications: _.pluck(this.notifications.notifications, 'id')
|
||||
});
|
||||
|
||||
_.each(this.notifications.notifications, notification => {
|
||||
notification.read = 1;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send a customer support request.
|
||||
*/
|
||||
sendSupportRequest() {
|
||||
Spark.post('/support/email', this.supportForm)
|
||||
.then(() => {
|
||||
$('#modal-support').modal('hide');
|
||||
|
||||
this.showSupportRequestSuccessMessage();
|
||||
|
||||
this.supportForm.subject = '';
|
||||
this.supportForm.message = '';
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show an alert informing the user their support request was sent.
|
||||
*/
|
||||
showSupportRequestSuccessMessage() {
|
||||
swal({
|
||||
title: 'Got It!',
|
||||
text: 'We have received your message and will respond soon!',
|
||||
type: 'success',
|
||||
showConfirmButton: false,
|
||||
timer: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine if the user has any unread notifications.
|
||||
*/
|
||||
hasUnreadAnnouncements() {
|
||||
if (this.notifications && this.user) {
|
||||
if (this.notifications.announcements.length && ! this.user.last_read_announcements_at) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.filter(this.notifications.announcements, announcement => {
|
||||
return moment.utc(this.user.last_read_announcements_at).isBefore(
|
||||
moment.utc(announcement.created_at)
|
||||
);
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any unread notifications.
|
||||
*/
|
||||
hasUnreadNotifications() {
|
||||
if (this.notifications) {
|
||||
return _.filter(this.notifications.notifications, notification => {
|
||||
return ! notification.read;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
25
spark/resources/assets/js/vue-bootstrap.js
vendored
Normal file
25
spark/resources/assets/js/vue-bootstrap.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Load Vue & Vue-Resource.
|
||||
*
|
||||
* Vue is the JavaScript framework used by Spark.
|
||||
*/
|
||||
if (window.Vue === undefined) {
|
||||
window.Vue = require('vue');
|
||||
|
||||
window.Bus = new Vue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Vue Global Mixin.
|
||||
*/
|
||||
Vue.mixin(require('./mixin'));
|
||||
|
||||
/**
|
||||
* Define the Vue filters.
|
||||
*/
|
||||
require('./filters');
|
||||
|
||||
/**
|
||||
* Load the Spark form utilities.
|
||||
*/
|
||||
require('./forms/bootstrap');
|
Reference in New Issue
Block a user