frontend & backend update

This commit is contained in:
carodej
2020-06-19 08:03:29 +02:00
parent 95bc310def
commit a2cab6198e
83 changed files with 4464 additions and 1907 deletions
+2 -12
View File
@@ -27,12 +27,7 @@
</td>
<td>
<span class="cell-item">
{{ row.data.attributes.payments_processed }}
</span>
</td>
<td>
<span class="cell-item">
{{ row.data.attributes.active_subscribers }}
{{ row.data.attributes.payment_processed }}
</span>
</td>
<td>
@@ -100,12 +95,7 @@
},
{
label: 'Payments Processed',
field: 'data.attributes.payments_processed',
sortable: true
},
{
label: 'Active Subscribers',
field: 'data.attributes.active_subscribers',
field: 'data.attributes.payment_processed',
sortable: true
},
{
@@ -1,47 +1,68 @@
<template>
<PageTab>
<PageTabGroup>
<DatatableWrapper :paginator="true" :columns="columns" :data="invoices" class="table">
<PageTabGroup v-if="transactions.length > 0">
<DatatableWrapper :paginator="true" :columns="columns" :data="transactions" class="table">
<template scope="{ row }">
<tr>
<td>
<span class="cell-item">
${{ row.attributes.total }}
</span>
<a :href="'/invoice/' + row.data.attributes.token" target="_blank" class="cell-item">
{{ row.data.attributes.order }}
</a>
</td>
<td>
<span class="cell-item">
{{ row.attributes.plan }}
</span>
<span class="cell-item">
${{ row.data.attributes.total }}
</span>
</td>
<td>
<span class="cell-item">
{{ row.attributes.created_at_formatted }}
</span>
<span class="cell-item">
{{ row.data.attributes.bag[0].description }}
</span>
</td>
<td>
<span class="cell-item">
{{ row.data.attributes.created_at_formatted }}
</span>
</td>
<td>
<router-link :to="{name: 'UserInvoices', params: {id: row.relationships.user.data.id}}">
<DatatableCellImage
image-size="small"
:image="row.relationships.user.data.attributes.avatar"
:title="row.relationships.user.data.attributes.name"
/>
</router-link>
</td>
<td>
<div class="action-icons">
<download-cloud-icon size="15" class="icon"></download-cloud-icon>
<a :href="'/invoice/' + row.data.attributes.token" target="_blank">
<external-link-icon size="15" class="icon"></external-link-icon>
</a>
</div>
</td>
</tr>
</template>
</DatatableWrapper>
</PageTabGroup>
<PageTabGroup v-else>
You don't have any transactions yet.
</PageTabGroup>
</PageTab>
</template>
<script>
import DatatableCellImage from '@/components/Others/Tables/DatatableCellImage'
import DatatableWrapper from '@/components/Others/Tables/DatatableWrapper'
import PageTabGroup from '@/components/Others/Layout/PageTabGroup'
import PageTab from '@/components/Others/Layout/PageTab'
import {DownloadCloudIcon} from "vue-feather-icons";
import {ExternalLinkIcon} from "vue-feather-icons";
import axios from 'axios'
export default {
name: 'GatewayTransactions',
components: {
DownloadCloudIcon,
DatatableCellImage,
ExternalLinkIcon,
DatatableWrapper,
PageTabGroup,
PageTab,
@@ -49,97 +70,13 @@
data() {
return {
isLoading: false,
invoices: [
{
id: '1',
type: 'invoices',
attributes: {
total: 9.99,
plan: 'Starter Plan',
created_at: '30. April. 2020',
created_at_formatted: '30. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '2',
type: 'invoices',
attributes: {
total: 9.99,
plan: 'Starter Plan',
created_at: '30. April. 2020',
created_at_formatted: '30. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '3',
type: 'invoices',
attributes: {
total: 49.99,
plan: 'Business Plan',
created_at: '31. April. 2020',
created_at_formatted: '31. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '4',
type: 'invoices',
attributes: {
total: 29.99,
plan: 'Professional Plan',
created_at: '31. April. 2020',
created_at_formatted: '31. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '5',
type: 'invoices',
attributes: {
total: 9.99,
plan: 'Starter Plan',
created_at: '30. April. 2020',
created_at_formatted: '30. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '6',
type: 'invoices',
attributes: {
total: 9.99,
plan: 'Starter Plan',
created_at: '30. April. 2020',
created_at_formatted: '30. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '7',
type: 'invoices',
attributes: {
total: 49.99,
plan: 'Business Plan',
created_at: '31. April. 2020',
created_at_formatted: '31. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
{
id: '8',
type: 'invoices',
attributes: {
total: 29.99,
plan: 'Professional Plan',
created_at: '31. April. 2020',
created_at_formatted: '31. April. 2020',
download: 'https://vuefilemanager.com/',
},
},
],
transactions: [],
columns: [
{
label: 'Invoice Number',
field: 'attributes.total',
sortable: true
},
{
label: 'Total',
field: 'attributes.total',
@@ -155,6 +92,11 @@
field: 'attributes.created_at',
sortable: true
},
{
label: 'User',
field: 'relationships.user.data.id',
sortable: true
},
{
label: this.$t('admin_page_user.table.action'),
field: 'data.action',
@@ -164,11 +106,11 @@
}
},
created() {
/*axios.get('/api/users/' + this.$route.params.id + '/storage')
axios.get('/api/gateways/' + this.$route.params.slug + '/transactions')
.then(response => {
this.storage = response.data.data
this.transactions = response.data.data
this.isLoading = false
})*/
})
}
}
</script>
@@ -34,15 +34,6 @@
</ValidationProvider>
</div>
<!--Price-->
<div class="block-wrapper">
<label>Price:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" name="Plan price" rules="required" v-slot="{ errors }">
<input @input="$updateText('/plans/' + $route.params.id + '/update', 'price', plan.attributes.price)" v-model="plan.attributes.price" placeholder="Plan price" type="number" step="0.01" min="1" max="99999" :class="{'is-error': errors[0]}"/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<!--Storage Capacity-->
<div class="block-wrapper">
<label>Storage Capacity:</label>
+42 -40
View File
@@ -35,8 +35,8 @@
</ColorLabel>
</td>
<td v-if="config.isSaaS">
<span class="cell-item" v-if="row.relationships.subscription">
{{ row.relationships.subscription.data.attributes.name }}
<span class="cell-item" v-if="row.data.attributes.subscription">
Premium
</span>
<span class="cell-item" v-else>
Free
@@ -114,44 +114,7 @@
return {
isLoading: true,
users: [],
columns: [
{
label: this.$t('admin_page_user.table.name'),
field: 'data.attributes.name',
sortable: true
},
{
label: this.$t('admin_page_user.table.role'),
field: 'data.attributes.role',
sortable: true
},
{
label: 'Subscription Plan',
field: 'data.attributes.role',
sortable: true,
hidden: true,
},
{
label: this.$t('admin_page_user.table.storage_used'),
field: 'data.attributes.storage.used',
sortable: true
},
{
label: this.$t('admin_page_user.table.storage_capacity'),
field: 'data.attributes.storage.capacity',
sortable: true
},
{
label: this.$t('admin_page_user.table.created_at'),
field: 'data.attributes.created_at_formatted',
sortable: true
},
{
label: this.$t('admin_page_user.table.action'),
field: 'data.action',
sortable: false
},
],
columns: undefined,
}
},
methods: {
@@ -167,6 +130,45 @@
}
},
created() {
this.columns = [
{
label: this.$t('admin_page_user.table.name'),
field: 'data.attributes.name',
sortable: true
},
{
label: this.$t('admin_page_user.table.role'),
field: 'data.attributes.role',
sortable: true
},
{
label: 'Subscription Plan',
field: 'data.attributes.role',
sortable: true,
hidden: ! this.config.isSaaS,
},
{
label: this.$t('admin_page_user.table.storage_used'),
field: 'data.attributes.storage.used',
sortable: true
},
{
label: this.$t('admin_page_user.table.storage_capacity'),
field: 'data.attributes.storage.capacity',
sortable: true
},
{
label: this.$t('admin_page_user.table.created_at'),
field: 'data.attributes.created_at_formatted',
sortable: true
},
{
label: this.$t('admin_page_user.table.action'),
field: 'data.action',
sortable: false
},
]
axios.get('/api/users')
.then(response => {
this.users = response.data.data
+15 -4
View File
@@ -26,7 +26,7 @@
</div>
</div>
<div v-if="config.isSaaS" class="headline-actions">
<router-link :to="{name: 'UpgradePlan'}">
<router-link :to="{name: 'UpgradePlan'}" v-if="! user.relationships.subscription || (user.relationships.subscription && ! user.relationships.subscription.data.attributes.is_highest)">
<ButtonBase button-style="secondary" type="button">
Upgrade Plan
</ButtonBase>
@@ -56,13 +56,22 @@
<router-link v-if="config.isSaaS" replace :to="{name: 'Subscription'}" class="menu-list-item link">
<div class="icon">
<credit-card-icon size="17"></credit-card-icon>
<cloud-icon size="17"></cloud-icon>
</div>
<div class="label">
Subscription
</div>
</router-link>
<router-link v-if="config.isSaaS" replace :to="{name: 'PaymentCards'}" class="menu-list-item link">
<div class="icon">
<credit-card-icon size="17"></credit-card-icon>
</div>
<div class="label">
Payment Cards
</div>
</router-link>
<router-link v-if="config.isSaaS" replace :to="{name: 'Invoice'}" class="menu-list-item link">
<div class="icon">
<file-text-icon size="17"></file-text-icon>
@@ -105,6 +114,7 @@
CreditCardIcon,
HardDriveIcon,
FileTextIcon,
CloudIcon,
UserIcon,
LockIcon,
} from 'vue-feather-icons'
@@ -112,6 +122,7 @@
export default {
name: 'Settings',
components: {
CloudIcon,
ButtonBase,
CreditCardIcon,
UserImageInput,
@@ -127,10 +138,10 @@
computed: {
...mapGetters(['user', 'config']),
subscriptionStatus() {
return this.user.relationships.subscription ? 'Premium' : 'Free'
return this.user.data.attributes.subscription ? 'Premium' : 'Free'
},
subscriptionColor() {
return this.user.relationships.subscription ? 'green' : 'purple'
return this.user.data.attributes.subscription ? 'green' : 'purple'
},
},
data() {
+424 -138
View File
@@ -13,118 +13,186 @@
</div>
<div class="order">
<div class="billing" v-if="billing">
<b class="form-group-label">Billing Information:</b>
<ValidationObserver ref="order" v-slot="{ invalid }" tag="form" class="form block-form">
<div class="form block-form">
<div class="block-wrapper">
<label>Name:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" rules="required"
name="billing_name" v-slot="{ errors }">
<input v-model="billing.billing_name"
placeholder="Type your billing name"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="steps">
<div class="block-wrapper">
<label>Address:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" rules="required"
name="billing_address" v-slot="{ errors }">
<input v-model="billing.billing_address"
placeholder="Type your billing address"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="payment-card">
<b class="form-group-label">Payment Card:</b>
<div class="block-wrapper">
<label>State:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" rules="required"
name="billing_state" v-slot="{ errors }">
<input v-model="billing.billing_state"
placeholder="Type your billing state"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<!-- Pay by new credit card -->
<div class="register-card" v-show="! defaultPaymentCard || payByNewCard">
<p class="payment-demo-disclaimer">
For test your payment please use <b>4242 4242 4242 4242</b> as a card number, <b>11/22</b>
as the expiration date and <b>123</b> as CVC number and ZIP <b>12345</b>.
</p>
<div class="wrapper-inline">
<div class="block-wrapper">
<label>City:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required" name="billing_city" v-slot="{ errors }">
<input v-model="billing.billing_city"
placeholder="Type your billing city"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div ref="stripeCard" class="stripe-card" :class="{'is-error': isError }"></div>
<div class="block-wrapper">
<label>Postal Code:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required" name="billing_postal_code"
v-slot="{ errors }">
<input v-model="billing.billing_postal_code"
placeholder="Type your billing postal code"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
<div class="block-wrapper">
<label>Country:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" rules="required"
name="billing_country" v-slot="{ errors }">
<input v-model="billing.billing_country"
placeholder="Type your billing country"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Phone Number:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper" rules="required"
name="billing_phone_number" v-slot="{ errors }">
<input v-model="billing.billing_phone_number"
placeholder="Type your billing phone number"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
<div class="card-error-message" v-if="isError">
<span>{{ errorMessage }}</span>
</div>
</div>
</ValidationObserver>
<!--User registered payment card-->
<div class="registered-cards" v-if="defaultPaymentCard && ! payByNewCard">
<div class="credit-card" :class="{'is-error': isError}">
<div class="card-number">
<img class="credit-card-icon"
:src="$getCreditCardBrand(defaultPaymentCard.data.attributes.brand)"
:alt="defaultPaymentCard.data.attributes.brand">
<div class="credit-card-numbers">
{{ defaultPaymentCard.data.attributes.last4 }}
</div>
<ColorLabel color="purple">Default</ColorLabel>
</div>
<div class="expiration-date">
<span>{{ defaultPaymentCard.data.attributes.exp_month }} / {{ defaultPaymentCard.data.attributes.exp_year }}</span>
</div>
</div>
<!--Change payment-->
<div class="change-payment" v-if="! isError">
<span>Also you can</span>
<router-link v-if="paymentCards.data.length > 0" :to="{name: 'PaymentCards'}">change your
default payment method
</router-link>
<span v-if="paymentCards.data.length > 0">or</span>
<a @click="payByNewCardForm">pay by new credit card.</a>
</div>
<!--Card error-->
<div class="card-error-message" v-if="isError">
<span>{{ errorMessage }}</span>
<span @click="payByNewCardForm"
class="link">Please pay by another payment card</span>
<span> or </span>
<router-link :to="{name: 'PaymentCards'}" class="link">Change your default payment
method
</router-link>
</div>
</div>
</div>
<div class="billing" v-if="billing">
<b class="form-group-label">Billing Information:</b>
<ValidationObserver ref="order" v-slot="{ invalid }" tag="form" class="form block-form">
<div class="form block-form">
<div class="block-wrapper">
<label>Name:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required"
name="billing_name" v-slot="{ errors }">
<input v-model="billing.billing_name"
placeholder="Type your billing name"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Address:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required"
name="billing_address" v-slot="{ errors }">
<input v-model="billing.billing_address"
placeholder="Type your billing address"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>State:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required"
name="billing_state" v-slot="{ errors }">
<input v-model="billing.billing_state"
placeholder="Type your billing state"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="wrapper-inline">
<div class="block-wrapper">
<label>City:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required" name="billing_city"
v-slot="{ errors }">
<input v-model="billing.billing_city"
placeholder="Type your billing city"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Postal Code:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required" name="billing_postal_code"
v-slot="{ errors }">
<input v-model="billing.billing_postal_code"
placeholder="Type your billing postal code"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
<div class="block-wrapper">
<label>Country:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required"
name="billing_country" v-slot="{ errors }">
<input v-model="billing.billing_country"
placeholder="Type your billing country"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Phone Number:</label>
<ValidationProvider tag="div" mode="passive" class="input-wrapper"
rules="required"
name="billing_phone_number" v-slot="{ errors }">
<input v-model="billing.billing_phone_number"
placeholder="Type your billing phone number"
type="text"
:class="{'is-error': errors[0]}"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</ValidationObserver>
</div>
</div>
<div class="summary">
<b class="form-group-label">Order Summary:</b>
<div class="summary-list" v-if="requestedPlan">
<div class="summary-list" :class="{'is-error': isError}" v-if="requestedPlan">
<div class="row">
<div class="cell">
<b>{{ requestedPlan.data.attributes.name }}</b>
<small>Billed monthly</small>
</div>
<div class="cell">
<b>{{ requestedPlan.data.attributes.price }} USD</b>
<b>{{ requestedPlan.data.attributes.price }}</b>
</div>
</div>
<div class="row">
@@ -132,15 +200,14 @@
<b>Total</b>
</div>
<div class="cell">
<b>{{ requestedPlan.data.attributes.price }} USD</b>
<b>{{ requestedPlan.data.attributes.price }}</b>
</div>
</div>
<ButtonBase :disabled="isSubmitted" :loading="isSubmitted" @click.native="submitOrder"
type="submit" button-style="theme-solid" class="next-submit">
Pay Order
Pay with credit card
</ButtonBase>
<p class="error-message" v-if="isError">{{ errorMessage }}</p>
<small class="disclaimer">
By submit form, you agree to save the payment method and billing information in your
VueFileManager account.
@@ -148,7 +215,6 @@
</div>
</div>
</div>
</div>
</div>
<div id="loader" v-if="isLoading">
@@ -162,14 +228,19 @@
import PlanPricingTables from '@/components/Others/PlanPricingTables'
import MobileHeader from '@/components/Mobile/MobileHeader'
import ButtonBase from '@/components/FilesView/ButtonBase'
import ColorLabel from '@/components/Others/ColorLabel'
import PageHeader from '@/components/Others/PageHeader'
import Spinner from '@/components/FilesView/Spinner'
import {CreditCardIcon} from 'vue-feather-icons'
import {required} from 'vee-validate/dist/rules'
import { mapGetters } from 'vuex'
import {mapGetters} from 'vuex'
import {events} from "@/bus"
import axios from 'axios'
let stripe = Stripe(`pk_test_51GsACaCBETHMUxzVsYkeApHtqb85paMuye7G77PDDQ28kXqDJ5HTmqLi13aM6xee81OQK1fhkTZ7vmDiWLStU9160061Yb2MtL`),
elements = stripe.elements(),
card = undefined;
export default {
name: 'UpgradePlan',
components: {
@@ -180,20 +251,66 @@
MobileHeader,
ButtonBase,
PageHeader,
ColorLabel,
required,
Spinner,
},
computed: {
...mapGetters(['requestedPlan']),
billing() {
return this.$store.getters.user.relationships.settings.data.attributes
}
},
data() {
return {
complete: false,
stripeOptions: {
hidePostalCode: false
},
isLoading: true,
isSubmitted: false,
billing: undefined,
paymentCards: undefined,
defaultPaymentCard: undefined,
errorMessage: undefined,
isError: false,
payByNewCard: false,
clientSecret: undefined
}
},
methods: {
payByNewCardForm() {
this.payByNewCard = true
this.isError = false
},
successOrder() {
// Update user data
//this.$store.dispatch('getAppData')
// End loading
this.isSubmitted = false
// Show toaster
events.$emit('toaster', {
type: 'success',
message: 'Your account was successfully upgraded.',
})
// Go to User page
//this.$router.push({name: 'Subscription'})
},
errorOrder(error) {
if (error.response.status = 402) {
this.isError = true
this.errorMessage = error.response.data.message
}
// End loading
this.isSubmitted = false
},
async submitOrder() {
// Validate fields
@@ -201,56 +318,89 @@
if (!isValid) return;
// Remove error
this.isError = false
// Start loading
this.isSubmitted = true
// Send order request
axios
.post('/api/subscription/upgrade', {
billing: this.billing,
plan: this.requestedPlan,
// If user don't have credit card, register new
if (!this.defaultPaymentCard || this.payByNewCard) {
console.log('Payment by new card');
const {setupIntent, error} = await stripe.confirmCardSetup(this.clientSecret, {
payment_method: {
card: card,
}
})
.then(() => {
// Update user data
this.$store.dispatch('getAppData')
if (error) {
// End loading
// Set error on
this.isError = true
// End button spinner
this.isSubmitted = false
// Show toaster
events.$emit('toaster', {
type: 'success',
message: 'Your account was successfully upgraded.',
// Show error message
this.errorMessage = error.message
} else {
axios
.post('/api/subscription/upgrade', {
billing: this.billing,
plan: this.requestedPlan,
payment: {
type: 'stripe',
meta: {
pm: setupIntent.payment_method,
}
}
})
.then(() => this.successOrder())
.catch((error) => this.errorOrder(error))
}
}
// if user has credit card
if (this.defaultPaymentCard && !this.payByNewCard) {
console.log('Payment by default card');
axios
.post('/api/subscription/upgrade', {
billing: this.billing,
plan: this.requestedPlan,
payment: {
type: 'stripe',
}
})
// Go to User page
this.$router.push({name: 'Storage'})
})
.catch(error => {
// End loading
this.isSubmitted = false
})
}
.then(() => this.successOrder())
.catch((error) => this.errorOrder(error))
}
},
},
mounted: function () {
card = elements.create('card');
card.mount(this.$refs.stripeCard);
},
created() {
axios.get('/api/user')
// Get setup intent for stripe
axios.get('/api/stripe/setup-intent')
.then(response => this.clientSecret = response.data.client_secret)
axios.get('/api/user/payments')
.then(response => {
if (! this.requestedPlan) {
if (!this.requestedPlan) {
this.$router.push({name: 'UpgradePlan'})
}
this.billing = {
billing_name: response.data.relationships.settings.data.attributes.billing_name,
billing_address: response.data.relationships.settings.data.attributes.billing_address,
billing_state: response.data.relationships.settings.data.attributes.billing_state,
billing_city: response.data.relationships.settings.data.attributes.billing_city,
billing_postal_code: response.data.relationships.settings.data.attributes.billing_postal_code,
billing_country: response.data.relationships.settings.data.attributes.billing_country,
billing_phone_number: response.data.relationships.settings.data.attributes.billing_phone_number,
}
this.defaultPaymentCard = response.data.default
this.paymentCards = response.data.others
this.isLoading = false
})
@@ -263,6 +413,133 @@
@import '@assets/vue-file-manager/_mixins';
@import '@assets/vue-file-manager/_forms';
.change-payment {
padding-top: 10px;
span {
font-weight: 600;
}
a {
cursor: pointer;
font-weight: 700;
&:hover {
text-decoration: underline;
}
}
span, a {
color: $text-muted;
@include font-size(14);
}
}
.card-error-message {
padding-top: 10px;
span, a {
@include font-size(14);
font-weight: 600;
color: $danger;
}
.link, a {
text-decoration: underline;
cursor: pointer;
&:hover {
text-decoration: none;
}
}
}
.registered-cards {
margin-bottom: 50px;
}
.register-card {
margin-bottom: 55px;
}
.credit-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
background: $light_background;
border-radius: 8px;
margin-top: 20px;
&.is-error {
box-shadow: 0 0 7px rgba($danger, 0.3);
border: 2px solid $danger;
border-radius: 4px;
}
span {
font-weight: 700;
}
.card-number {
display: flex;
}
.credit-card-numbers {
vertical-align: middle;
margin-right: 10px;
}
.credit-card-icon {
vertical-align: middle;
max-height: 20px;
margin-right: 8px;
}
}
.payment-demo-disclaimer {
padding: 15px;
background: $light_background;
border-radius: 8px;
margin-bottom: 20px;
line-height: 1.6;
b {
color: $danger;
}
}
.stripe-card {
box-sizing: border-box;
padding: 13px 20px;
border: 1px solid transparent;
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
&.is-error {
box-shadow: 0 0 7px rgba($danger, 0.3);
border: 2px solid $danger;
border-radius: 4px;
}
&.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
&.StripeElement--invalid {
border-color: #fa755a;
}
&.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
}
.summary-list {
box-shadow: 0 7px 20px 5px hsla(220, 36%, 16%, 0.06);
border-radius: 8px;
@@ -270,6 +547,15 @@
padding: 25px;
top: 30px;
&.is-error {
border: 2px solid $danger;
box-shadow: 0 7px 20px 5px rgba($danger, 0.06);
}
.error-message {
font-weight: 600;
}
.next-submit {
width: 100%;
margin-top: 20px;
@@ -317,7 +603,7 @@
.order {
display: flex;
.billing {
.steps {
flex: 0 0 65%;
padding-right: 30px;
@@ -49,6 +49,12 @@
this.$store.commit('STORE_REQUESTED_PLAN', plan)
}
},
/*beforeMount() {
let StripeElementsScript = document.createElement('script')
StripeElementsScript.setAttribute('src', 'https://js.stripe.com/v3/')
document.head.appendChild(StripeElementsScript)
},*/
}
</script>
+244
View File
@@ -0,0 +1,244 @@
<template>
<PageTab :is-loading="isLoading">
<PageTabGroup v-if="paymentCards && paymentCards.length > 0">
<DatatableWrapper :paginator="true" :columns="columns" :data="paymentCards" class="table">
<template scope="{ row }">
<tr :class="{'is-deleting': row.data.attributes.card_id === deletingID}">
<td style="width: 300px">
<span class="cell-item">
<div class="credit-card">
<img class="credit-card-icon" :src="$getCreditCardBrand(row.data.attributes.brand)"
:alt="row.data.attributes.brand">
<div class="credit-card-numbers">
{{ row.data.attributes.last4 }}
</div>
<ColorLabel v-if="row.data.id === defaultPaymentCard.data.id" color="purple">Default</ColorLabel>
</div>
</span>
</td>
<td>
<span class="cell-item">
<ColorLabel :color="getCardStatusColor(row.data.attributes.status)">{{ getCardStatus(row.data.attributes.status) }}</ColorLabel>
</span>
</td>
<td>
<span class="cell-item">
{{ row.data.attributes.exp_month }} / {{ row.data.attributes.exp_year }}
</span>
</td>
<td>
<div class="action-icons">
<credit-card-icon size="15" class="icon icon-card" title="Set as default card" @click="setDefaultCard(row.data.attributes)" v-if="row.data.id !== defaultPaymentCard.data.id"></credit-card-icon>
<trash2-icon size="15" class="icon icon-trash" title="Delete card" @click="deleteCard(row.data.attributes)"></trash2-icon>
</div>
</td>
</tr>
</template>
</DatatableWrapper>
</PageTabGroup>
<PageTabGroup v-else>
You don't have any payment cards yet.
</PageTabGroup>
</PageTab>
</template>
<script>
import DatatableWrapper from '@/components/Others/Tables/DatatableWrapper'
import PageTabGroup from '@/components/Others/Layout/PageTabGroup'
import PageTab from '@/components/Others/Layout/PageTab'
import ColorLabel from '@/components/Others/ColorLabel'
import {CreditCardIcon, Trash2Icon} from "vue-feather-icons"
import {events} from "@/bus"
import axios from 'axios'
export default {
name: 'UserPaymentCards',
components: {
DatatableWrapper,
PageTabGroup,
Trash2Icon,
ColorLabel,
CreditCardIcon,
PageTab,
},
data() {
return {
defaultPaymentCard: undefined,
paymentCards: undefined,
deletingID: undefined,
columns: [
{
label: 'Card Number',
field: 'data.attributes.total',
sortable: true
},
{
label: 'Status',
field: 'data.attributes.status',
sortable: true
},
{
label: 'Expiration Date',
field: 'data.attributes.total',
sortable: true
},
{
label: this.$t('admin_page_user.table.action'),
field: 'data.action',
sortable: false
},
],
isLoading: true,
}
},
methods: {
getCardStatusColor(status) {
switch (status) {
case 'active':
return 'green'
break
case 'card_declined':
return 'yellow'
break
case 'expired':
return 'red'
break
}
},
getCardStatus(status) {
switch (status) {
case 'active':
return 'Active'
break
case 'card_declined':
return 'Rejected'
break
case 'expired':
return 'Expired'
break
}
},
setDefaultCard(card) {
events.$emit('confirm:open', {
title: 'Set as default card?',
message: 'Your card will be set as default and will be always charged for the next billings.',
buttonColor: 'theme-solid',
action: {
id: card.card_id,
operation: 'set-as-default-credit-card'
}
})
},
deleteCard(card) {
events.$emit('confirm:open', {
title: 'Are you sure?',
message: 'This event is irreversible and your payment card will be delete forever',
action: {
id: card.card_id,
operation: 'delete-credit-card'
}
})
},
fetchPaymentCards() {
axios.get('/api/user/payments')
.then(response => {
this.defaultPaymentCard = response.data.default
this.paymentCards = response.data.others.data
this.paymentCards.push(response.data.default)
this.isLoading = false
})
}
},
created() {
// Get payments card
this.fetchPaymentCards()
// Delete credit card
events.$on('action:confirmed', data => {
if (data.operation === 'delete-credit-card') {
this.deletingID = data.id
axios.delete('/api/user/payment-cards/' + data.id)
.then(() => {
// Get payments card
this.fetchPaymentCards()
// Show toaster
events.$emit('toaster', {
type: 'success',
message: 'Your card was successfully deleted.',
})
})
.catch(error => {
console.error(error);
})
}
if (data.operation === 'set-as-default-credit-card') {
axios.patch('/api/user/payment-cards/' + data.id, {
default: 1
})
.then(() => {
// Get payments card
this.fetchPaymentCards()
// Show toaster
events.$emit('toaster', {
type: 'success',
message: 'Your card was successfully set as default.',
})
})
.catch(error => {
console.error(error);
})
}
})
},
destroyed() {
events.$off('action:confirmed')
}
}
</script>
<style lang="scss" scoped>
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
@import '@assets/vue-file-manager/_forms';
.is-deleting {
opacity: 0.35;
}
.credit-card {
display: flex;
align-items: center;
.credit-card-numbers {
margin-right: 10px;
}
.credit-card-icon {
max-height: 20px;
margin-right: 8px;
}
}
@media only screen and (max-width: 960px) {
}
@media (prefers-color-scheme: dark) {
}
</style>
+90 -20
View File
@@ -1,34 +1,44 @@
<template>
<PageTab>
<PageTab :is-loading="isLoading">
<PageTabGroup v-if="subscription">
<!--Info about active subscription-->
<div v-if="! subscription.canceled" class="state active">
<div v-if="! subscription.data.attributes.canceled" class="state active">
<ListInfo class="list-info">
<ListInfoItem class="list-item" title="Plan" :content="subscription.name + ' - ' + subscription.capacity_formatted"/>
<ListInfoItem class="list-item" title="Plan" :content="subscription.data.attributes.name + ' - ' + subscription.data.attributes.capacity_formatted"/>
<ListInfoItem class="list-item" title="Billed" content="Monthly"/>
<ListInfoItem class="list-item" title="Status" :content="status"/>
<ListInfoItem class="list-item" title="Created At" :content="subscription.created_at"/>
<ListInfoItem class="list-item" title="Renews At" :content="subscription.ends_at"/>
<ListInfoItem class="list-item" title="Created At" :content="subscription.data.attributes.created_at"/>
<ListInfoItem class="list-item" title="Renews At" :content="subscription.data.attributes.ends_at"/>
</ListInfo>
<div class="cancel-plan">
<div class="plan-action">
<ButtonBase
:disabled="isDeleting"
@click.native="cancelSubscription"
:button-style="cancelButtonStyle"
class="cancel-button">
class="confirm-button">
{{ cancelButtonText }}
</ButtonBase>
</div>
</div>
<!--Info about canceled subscription-->
<div v-if="subscription.canceled" class="state canceled">
<div v-if="subscription.data.attributes.canceled" class="state canceled">
<ListInfo class="list-info">
<ListInfoItem class="list-item" title="Plan" :content="subscription.name"/>
<ListInfoItem class="list-item" title="Plan" :content="subscription.data.attributes.name"/>
<ListInfoItem class="list-item" title="Status" :content="status"/>
<ListInfoItem class="list-item" title="Canceled At" :content="subscription.canceled_at"/>
<ListInfoItem class="list-item" title="Ends At" :content="subscription.ends_at"/>
<ListInfoItem class="list-item" title="Canceled At" :content="subscription.data.attributes.canceled_at"/>
<ListInfoItem class="list-item" title="Ends At" :content="subscription.data.attributes.ends_at"/>
</ListInfo>
<div class="plan-action">
<ButtonBase
:disabled="isResuming"
@click.native="resumeSubscription"
:button-style="resumeButtonStyle"
class="confirm-button">
{{ resumeButtonText }}
</ButtonBase>
</div>
</div>
</PageTabGroup>
<PageTabGroup v-else>
@@ -65,26 +75,31 @@
return this.isConfirmedCancel ? this.$t('popup_share_edit.confirm') : 'Cancel Plan'
},
cancelButtonStyle() {
return this.isConfirmedCancel ? 'danger-solid' : 'danger'
return this.isConfirmedCancel ? 'danger-solid' : 'secondary'
},
subscription() {
return this.$store.getters.user.relationships.subscription
? this.$store.getters.user.relationships.subscription.data.attributes
: undefined
resumeButtonText() {
return this.isConfirmedResume ? this.$t('popup_share_edit.confirm') : 'Resume Plan'
},
resumeButtonStyle() {
return this.isConfirmedResume ? 'theme-solid' : 'secondary'
},
status() {
if (this.subscription.canceled) {
if (this.subscription.data.attributes.canceled) {
return 'Canceled'
}
if (this.subscription.active) {
if (this.subscription.data.attributes.active) {
return 'Active'
}
}
},
data() {
return {
subscription: undefined,
isConfirmedCancel: false,
isConfirmedResume: false,
isDeleting: false,
isResuming: false,
isLoading: true,
}
},
methods: {
@@ -98,6 +113,7 @@
// Start deleting spinner button
this.isDeleting = true
this.isLoading = true
// Send delete request
axios
@@ -105,7 +121,9 @@
.then(() => {
// Update user data
this.$store.dispatch('getAppData')
this.$store.dispatch('getAppData').then(() => {
this.fetchSubscriptionDetail()
})
// End deleting spinner button
this.isDeleting = false
@@ -122,9 +140,61 @@
// End deleting spinner button
this.isDeleting = false
this.isLoading = false
})
}
},
resumeSubscription() {
// Set confirm button
if (! this.isConfirmedResume) {
this.isConfirmedResume = true
} else {
// Start deleting spinner button
this.isResuming = true
this.isLoading = true
// Send delete request
axios
.post('/api/subscription/resume')
.then(() => {
// Update user data
this.$store.dispatch('getAppData').then(() => {
this.fetchSubscriptionDetail()
})
// End deleting spinner button
this.isResuming = false
events.$emit('alert:open', {
emoji: '👍',
title: 'Subscription Was Resumed',
message: 'Your subscription was re-activated, and they will be billed on the original billing cycle.',
buttonStyle: 'theme',
button: 'That\'s awesome!'
})
})
.catch(() => {
// End deleting spinner button
this.isResuming = false
this.isLoading = false
})
}
},
fetchSubscriptionDetail() {
axios.get('/api/user/subscription')
.then(response => {
this.subscription = response.data
this.isLoading = false
})
}
},
created() {
this.fetchSubscriptionDetail()
}
}
</script>
@@ -133,7 +203,7 @@
@import '@assets/vue-file-manager/_variables';
@import '@assets/vue-file-manager/_mixins';
.cancel-plan {
.plan-action {
margin-top: 10px;
}