Initial Spark Install

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

View File

@@ -0,0 +1,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);
}
}
};

View 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;
}
}
};

View 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');
});
}
}
};

View 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`;
}
}
};

View 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}`;
}
}
};

View 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`;
}
}
};

View 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`;
}
}
}
}
};

View 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);
},
};

View 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`;
}
}
};

View 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';
}
}
}
};

View 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 '';
}
}
};

View 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`;
}
}
}

View File

@@ -0,0 +1,3 @@
module.exports = {
props: ['user']
};

View 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');
});
}
}
};

View 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})`;
}
}
};

View 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');
});
}
};

View 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');
});
}
}
};

View 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');
});
}
}
};

View 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);
}
}
};

View 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');
}
};

View 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"});
});
}
}
};

View 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`;
}
}
};

View 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));
}
}
};

View 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`;
}
}
};

View 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;
}
}
};

View 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));
}
}
};

View File

@@ -0,0 +1,3 @@
module.exports = {
props: ['user', 'teams'],
};

View 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;
});
}
}
};

View 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}`;
}
}
};

View 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');
});
}
}
};

View 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);
}
}
};

View 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;
});
}
}
};

View 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}`;
}
}
};

View 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;
});
}
}
};

View File

@@ -0,0 +1,3 @@
module.exports = {
props: ['user', 'team']
};

View 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;
});
}
}
};

View 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');
});
}
}
};

View 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})`;
}
}
};