Subscription UI refactoring

This commit is contained in:
Čarodej
2022-01-03 16:18:36 +01:00
parent 9d189b3d12
commit 09d8b84870
25 changed files with 1489 additions and 1575 deletions

View File

@@ -0,0 +1,86 @@
<template>
<PageTab>
<!-- Metered subscription components -->
<div v-if="config.subscriptionType === 'metered'">
<!--Failed Payments-->
<UserFailedPayments />
<!--Balance-->
<UserBalance/>
<!--Usage Estimates-->
<UserUsageEstimates />
<!--Billing Alert-->
<UserBillingAlerts />
<!-- Payment method for automatically handle payments - only for Stripe -->
<UserStoredPaymentMethods />
<!--Transactions-->
<UserTransactionsForMeteredBilling />
</div>
<!-- Fixed subscription components -->
<div v-if="config.subscriptionType === 'fixed'">
<!-- Subscription Detail -->
<UserFixedSubscriptionDetail />
<!-- Payment method for automatically handle payments - only for Stripe -->
<UserStoredPaymentMethods />
<!-- Update payment in external source -->
<UserUpdatePaymentMethodsExternally />
<!-- Edit subscription -->
<UserEditSubscription />
<!-- Transactions -->
<UserTransactionsForFixedBilling />
<!-- Empty subscription -->
<UserEmptySubscription />
</div>
</PageTab>
</template>
<script>
import UserUpdatePaymentMethodsExternally from "../../components/Subscription/UserUpdatePaymentMethodsExternally"
import UserTransactionsForMeteredBilling from "../../components/Subscription/UserTransactionsForMeteredBilling"
import UserTransactionsForFixedBilling from "../../components/Subscription/UserTransactionsForFixedBilling"
import UserFixedSubscriptionDetail from "../../components/Subscription/UserFixedSubscriptionDetail"
import UserStoredPaymentMethods from "../../components/Subscription/UserStoredPaymentMethods"
import UserEmptySubscription from "../../components/Subscription/UserEmptySubscription"
import UserEditSubscription from "../../components/Subscription/UserEditSubscription"
import UserFailedPayments from "../../components/Subscription/UserFailedPayments"
import UserUsageEstimates from "../../components/Subscription/UserUsageEstimates"
import UserBillingAlerts from "../../components/Subscription/UserBillingAlerts"
import PageTab from '/resources/js/components/Others/Layout/PageTab'
import UserBalance from "../../components/Subscription/UserBalance"
import {mapGetters} from 'vuex'
export default {
name: 'Billing',
components: {
UserUpdatePaymentMethodsExternally,
UserTransactionsForMeteredBilling,
UserTransactionsForFixedBilling,
UserFixedSubscriptionDetail,
UserStoredPaymentMethods,
UserEmptySubscription,
UserEditSubscription,
UserFailedPayments,
UserUsageEstimates,
UserBillingAlerts,
UserBalance,
PageTab,
},
computed: {
...mapGetters([
'config',
]),
},
}
</script>

View File

@@ -1,247 +0,0 @@
<template>
<PageTab>
<PageTabGroup>
<div class="form block-form">
<FormLabel>{{ $t('user_payments.add_card') }}</FormLabel>
<!-- Register new credit card -->
<div class="register-card">
<InfoBox v-if="config.isDemo || config.isDev">
<p>For test your payment please use <b class="text-theme">4242 4242 4242 4242</b> or <b class="text-theme">5555 5555 5555 4444</b> as a card number, <b class="text-theme">11/22</b>
as the expiration date and <b class="text-theme">123</b> as CVC number and ZIP <b class="text-theme">12345</b>.</p>
</InfoBox>
<div class="block-wrapper">
<label>{{ $t('user_payments.card_field_title') }}:</label>
<div ref="stripeCard" class="stripe-card" :class="{'is-error': isError }">
<span class="loading">
{{ $t('user_payments.field_loading') }}
</span>
</div>
<div class="card-error-message" v-if="isError">
<span>{{ errorMessage }}</span>
</div>
</div>
</div>
<div class="block-wrapper">
<div class="input-wrapper">
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label">{{ $t('user_add_card.default_title') }}:</label>
<small class="input-help">{{ $t('user_add_card.default_description') }}</small>
</div>
<SwitchInput v-model="defaultPaymentMethod" class="switch" :state="defaultPaymentMethod"/>
</div>
</div>
</div>
<ButtonBase @click.native="registerCard" :loading="isSubmitted" :button-style="isDisabledSubmit ? 'secondary' : 'theme'" type="submit">
{{ $t('user_payments.store_card') }}
</ButtonBase>
</div>
</PageTabGroup>
</PageTab>
</template>
<script>
import PageTabGroup from '/resources/js/components/Others/Layout/PageTabGroup'
import SwitchInput from '/resources/js/components/Others/Forms/SwitchInput'
import FormLabel from '/resources/js/components/Others/Forms/FormLabel'
import ButtonBase from '/resources/js/components/FilesView/ButtonBase'
import PageTab from '/resources/js/components/Others/Layout/PageTab'
import InfoBox from '/resources/js/components/Others/Forms/InfoBox'
import {mapGetters} from 'vuex'
import {events} from '/resources/js/bus'
import axios from 'axios'
let [stripe, card] = [undefined, undefined];
export default {
name: 'CreatePaymentMethod',
components: {
PageTabGroup,
SwitchInput,
ButtonBase,
FormLabel,
PageTab,
InfoBox,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
errorMessage: undefined,
isError: false,
stripeOptions: {
hidePostalCode: false
},
isSubmitted: false,
isDisabledSubmit: true,
defaultPaymentMethod: true,
clientSecret: undefined,
}
},
methods: {
async registerCard() {
// Prevent empty submit
if (! stripe && !card && ! this.$refs.stripeCard.classList.contains('StripeElement')) return
// Start loading
this.isSubmitted = true
const {setupIntent, error} = await stripe.confirmCardSetup(this.clientSecret, {
payment_method: {
card: card,
}
})
if (setupIntent) {
axios
.post('/api/user/subscription/payment-cards', {
token: setupIntent.payment_method,
default: this.defaultPaymentMethod,
})
.then(() => {
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.card_new_add'),
})
// Go to payment methods page
this.$router.push({name: 'PaymentMethods'})
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
.finally(() => {
this.isSubmitted = false
})
}
if (error) {
// Set error on
this.isError = true
// End button spinner
this.isSubmitted = false
// Show error message
this.errorMessage = error.message
}
},
initStripe() {
stripe = Stripe(this.config.stripe_public_key)
let elements = stripe.elements();
card = elements.create('card');
card.mount(this.$refs.stripeCard);
this.isDisabledSubmit = false
},
},
mounted() {
let StripeElementsScript = document.createElement('script')
StripeElementsScript.setAttribute('src', 'https://js.stripe.com/v3/')
document.head.appendChild(StripeElementsScript)
// Get setup intent for stripe
axios.get('/api/user/subscription/setup-intent')
.then(response => {
this.clientSecret = response.data.client_secret
this.initStripe()
})
},
}
</script>
<style lang="scss" scoped>
@import '/resources/sass/vuefilemanager/_variables';
@import '/resources/sass/vuefilemanager/_mixins';
@import '/resources/sass/vuefilemanager/_forms';
.register-card {
margin-bottom: 25px;
}
.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;
&:not(.StripeElement) {
background: $light_background;
padding: 14px 20px;
.loading {
@include font-size(14);
font-weight: 700;
}
}
&.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;
}
iframe .InputContainer .InputElement {
color: white;
}
}
.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;
}
}
}
</style>

View File

@@ -1,611 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<!--Failed Payments-->
<div v-if="user.data.relationships.failedPayments && user.data.relationships.failedPayments.data.length > 0" class="card shadow-card">
<FormLabel icon="frown">
{{ $t('Failed Payments') }}
</FormLabel>
<b class="text-3xl text-red font-extrabold -mt-3 block mb-0.5">
-{{ user.data.meta.totalDebt }}
</b>
<b class="mb-3 block text-sm text-gray-400 mb-5">
{{ $t('We are unable to charge your usage for items below') }}
</b>
<!--Failed Payments-->
<div
v-for="payment in user.data.relationships.failedPayments.data"
:key="payment.data.id"
class="flex items-center justify-between py-2 border-b dark:border-opacity-5 border-light border-dashed"
>
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ payment.data.attributes.note }}
</b>
</div>
<div class="text-left w-1/4">
<span class="text-sm font-bold text-gray-560 capitalize">
{{ $t(payment.data.attributes.source) }}
</span>
</div>
<div class="text-right w-1/4">
<span class="text-sm font-bold">
{{ payment.data.attributes.created_at }}
</span>
</div>
<div class="text-right w-1/4">
<span class="text-sm font-bold text-theme">
{{ payment.data.attributes.amount }}
</span>
</div>
</div>
<InfoBox type="error" class="mt-7" style="margin-bottom: 0">
<p v-html="$t('Uh-oh! We are unable to charge your usage. Please register new credit card or fund your account with sufficient amount and we\'ll give it another try!')"></p>
</InfoBox>
</div>
<!-- Balance -->
<div v-if="! hasPaymentMethod" class="card shadow-card">
<FormLabel icon="dollar">
{{ $t('Balance') }}
</FormLabel>
<b class="text-3xl font-extrabold -mt-3 block mb-0.5">
{{ user.data.relationships.balance.data.attributes.formatted }}
</b>
<!-- Make payment form -->
<ValidationObserver ref="fundAccount" @submit.prevent="makePayment" v-slot="{ invalid }" tag="form" class="mt-6">
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Capacity" rules="required">
<AppInputText :description="$t('The amount will be increased as soon as we register your charge from payment gateway.')" :error="errors[0]" :is-last="true">
<div class="sm:flex sm:space-x-4 sm:space-y-0 space-y-4">
<input v-model="chargeAmount"
:placeholder="$t('Fund Your Account Balance...')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{'border-red-700': errors[0]}"
/>
<ButtonBase type="submit" button-style="theme" class="sm:w-auto w-full">
{{ $t('Make a Payment') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
<!--Usage Estimates-->
<div class="card shadow-card">
<FormLabel icon="bar-chart">
{{ $t('Usage Estimates') }}
</FormLabel>
<b class="text-3xl font-extrabold -mt-3 block mb-0.5">
{{ user.data.meta.usages.costEstimate }}
</b>
<b class="mb-3 block text-sm text-gray-400 mb-5">
{{ user.data.relationships.subscription.data.attributes.updated_at }} {{ $t('till now') }}
</b>
<div>
<div class="flex items-center justify-between py-2 border-b dark:border-opacity-5 border-light border-dashed" v-for="(usage, i) in user.data.meta.usages.featureEstimates" :key="i">
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ $t(usage.feature) }}
</b>
<small class="text-xs text-gray-500 pt-2 leading-none sm:block hidden">
{{ $t(`feature_usage_desc_${usage.feature}`) }}
</small>
</div>
<div class="text-left w-1/4">
<span class="text-sm font-bold text-gray-560">
{{ usage.usage }}
</span>
</div>
<div class="text-right w-1/4">
<span class="text-sm font-bold text-theme">
{{ usage.cost }}
</span>
</div>
</div>
</div>
</div>
<!--Billing Alert-->
<div class="card shadow-card">
<FormLabel icon="bell">
{{ $t('Billing Alert') }}
</FormLabel>
<div v-if="user.data.relationships.alert">
<b class="text-3xl font-extrabold -mt-3 block mb-0.5 flex items-center">
{{ user.data.relationships.alert.data.attributes.formatted }}
<edit2-icon v-if="! showUpdateBillingAlertForm" @click="showUpdateBillingAlertForm = ! showUpdateBillingAlertForm" size="12" class="vue-feather cursor-pointer ml-2 transform -translate-y-0.5" />
<trash2-icon v-if="showUpdateBillingAlertForm" @click="deleteBillingAlert" size="12" class="vue-feather cursor-pointer ml-2 transform -translate-y-0.5" />
</b>
<b class="block text-sm text-gray-400">
{{ $t('Alert will be triggered after you reach the value above.') }}
</b>
</div>
<ValidationObserver v-if="showUpdateBillingAlertForm" ref="updatebillingAlertForm" @submit.prevent="updateBillingAlert" v-slot="{ invalid }" tag="form" class="mt-6">
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Billing Alert" rules="required">
<AppInputText :description="$t('You will receive an email whenever your monthly balance reaches the specified amount above.')" :error="errors[0]" :is-last="true">
<div class="sm:flex sm:space-x-4 sm:space-y-0 space-y-4">
<input v-model="billingAlertAmount"
:placeholder="$t('Alert Amount...')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{'border-red-700': errors[0]}"
/>
<ButtonBase :loadint="isSendingBillingAlert" :disabled="isSendingBillingAlert" type="submit" button-style="theme" class="sm:w-auto w-full">
{{ $t('Update Alert') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
<ValidationObserver v-if="! user.data.relationships.alert" ref="billingAlertForm" @submit.prevent="setBillingAlert" v-slot="{ invalid }" tag="form" class="mt-6">
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Billing Alert" rules="required">
<AppInputText :description="$t('You will receive an email whenever your monthly balance reaches the specified amount above.')" :error="errors[0]" :is-last="true">
<div class="flex space-x-4">
<input v-model="billingAlertAmount"
:placeholder="$t('Alert Amount...')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{'border-red-700': errors[0]}"
/>
<ButtonBase :loadint="isSendingBillingAlert" :disabled="isSendingBillingAlert" type="submit" button-style="theme" class="submit-button">
{{ $t('Set Alert') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
<!-- Payment method for automatically handle payments - only for Stripe -->
<div v-if="config.isStripe" class="card shadow-card">
<FormLabel icon="credit-card">
{{ $t('Payment Method') }}
</FormLabel>
<!-- User doesn't have registered payment method -->
<div v-if="! hasPaymentMethod">
<!-- Show credit card form -->
<ButtonBase @click.native="showStoreCreditCardForm" v-if="! isCreditCardForm" :loading="stripeData.storingStripePaymentMethod" type="submit" button-style="theme" class="w-full mt-4">
{{ $t('Add Auto Payment Method') }}
</ButtonBase>
<!-- Store credit card form -->
<form v-if="isCreditCardForm" @submit.prevent="storeStripePaymentMethod" id="payment-form" class="mt-6">
<div v-if="stripeData.isInitialization" class="h-10 relative mb-6">
<Spinner />
</div>
<div id="payment-element">
<!-- Elements will create form elements here -->
</div>
<ButtonBase :loading="stripeData.storingStripePaymentMethod" type="submit" button-style="theme" class="w-full mt-4">
{{ $t('Store My Credit Card') }}
</ButtonBase>
<div id="error-message">
<!-- Display error message to your customers here -->
</div>
</form>
</div>
<!-- User has registered payment method -->
<div v-if="hasPaymentMethod">
<b v-if="user.data.relationships.balance.data.attributes.balance > 0" class="mb-3 block text-sm mb-5">
{{ $t('credit_to_auto_withdraw', {credit: user.data.relationships.balance.data.attributes.formatted}) }}
</b>
<!-- Card -->
<div
v-for="card in user.data.relationships.creditCards.data"
:key="card.data.id"
class="flex items-center justify-between py-3 px-4 input-dark"
>
<div class="flex items-center">
<img :src="`/assets/gateways/${card.data.attributes.brand}.svg`" alt="" class="h-5 mr-3 rounded">
<b class="text-sm font-bold leading-none capitalize">
{{ card.data.attributes.brand }} •••• {{ card.data.attributes.last4 }}
</b>
</div>
<b class="text-sm font-bold leading-none">
{{ $t('Expires') }} {{ card.data.attributes.expiration }}
</b>
<Trash2Icon @click="deleteCreditCard(card.data.id)" size="15" class="cursor-pointer" />
</div>
<small class="text-xs text-gray-500 pt-3 leading-none sm:block hidden">
{{ $t('We are settling your payment automatically via your saved credit card.') }}
</small>
</div>
</div>
<!--Transactions-->
<div class="card shadow-card">
<FormLabel icon="file-text">
{{ $t('Transactions') }}
</FormLabel>
<DatatableWrapper
@init="isLoading = false"
api="/api/subscriptions/transactions"
:paginator="true"
:columns="columns"
>
<template slot-scope="{ row }">
<tr class="border-b dark:border-opacity-5 border-light border-dashed">
<td class="py-4">
<span class="text-sm font-bold">
{{ row.data.attributes.note }}
</span>
</td>
<td>
<ColorLabel class="capitalize" :color="$getTransactionStatusColor(row.data.attributes.status)">
{{ row.data.attributes.status }}
</ColorLabel>
</td>
<td class="py-4">
<span class="text-sm font-bold capitalize">
{{ $t(row.data.attributes.type) }}
</span>
</td>
<td>
<span class="text-sm font-bold" :class="$getTransactionTypeTextColor(row.data.attributes.type)">
{{ $getTransactionMark(row.data.attributes.type) + row.data.attributes.price }}
</span>
</td>
<td>
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="text-right">
<img class="inline-block max-h-5" :src="$getPaymentLogo(row.data.attributes.driver)" :alt="row.data.attributes.driver">
</td>
</tr>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox>
<p>{{ $t('admin_page_user.invoices.empty') }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</div>
</PageTab>
</template>
<script>
import {
CreditCardIcon,
XIcon,
Trash2Icon,
Edit2Icon,
} from "vue-feather-icons";
import Spinner from "../../components/FilesView/Spinner";
import ColorLabel from "../../components/Others/ColorLabel";
import DatatableWrapper from "../../components/Others/Tables/DatatableWrapper";
import AppInputText from "../../components/Admin/AppInputText";
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import ButtonBase from '/resources/js/components/FilesView/ButtonBase'
import PageTab from '/resources/js/components/Others/Layout/PageTab'
import InfoBox from '/resources/js/components/Others/Forms/InfoBox'
import AppInputSwitch from "../../components/Admin/AppInputSwitch"
import FormLabel from "../../components/Others/Forms/FormLabel"
import ProgressLine from "../../components/Admin/ProgressLine"
import {mapGetters} from 'vuex'
import axios from 'axios'
import {events} from "../../bus"
import {loadStripe} from '@stripe/stripe-js'
// Define stripe variables
let stripe, elements = undefined
export default {
name: 'MeteredSubscription',
components: {
CreditCardIcon,
DatatableWrapper,
ValidationProvider,
ValidationObserver,
AppInputSwitch,
AppInputText,
ProgressLine,
ButtonBase,
ColorLabel,
Trash2Icon,
Edit2Icon,
XIcon,
FormLabel,
InfoBox,
PageTab,
Spinner,
},
computed: {
...mapGetters([
'isDarkMode',
'config',
'user',
]),
hasPaymentMethod() {
return this.user.data.relationships.creditCards && this.user.data.relationships.creditCards.data.length > 0
},
},
data() {
return {
isLoading: false,
isSendingBillingAlert: false,
billingAlertAmount: undefined,
showUpdateBillingAlertForm: false,
columns: [
{
label: this.$t('Note'),
field: 'note',
sortable: true
},
{
label: this.$t('Status'),
field: 'status',
sortable: true
},
{
label: this.$t('Type'),
field: 'status',
sortable: true
},
{
label: this.$t('admin_page_invoices.table.total'),
field: 'amount',
sortable: true
},
{
label: this.$t('Payed At'),
field: 'created_at',
sortable: true
},
{
label: this.$t('Service'),
field: 'driver',
sortable: true
},
],
chargeAmount: undefined,
isCreditCardForm: false,
stripeData: {
isInitialization: true,
storingStripePaymentMethod: false
}
}
},
methods: {
async updateBillingAlert() {
// Validate fields
const isValid = await this.$refs.updatebillingAlertForm.validate();
if (!isValid) return;
this.isSendingBillingAlert = true
axios
.patch(`/api/subscriptions/billing-alerts/${this.user.data.relationships.alert.data.id}`, {
amount: this.billingAlertAmount
})
.then(() => {
this.$store.dispatch('getAppData')
this.showUpdateBillingAlertForm = false
events.$emit('toaster', {
type: 'success',
message: this.$t('Your billing alert was updated successfully'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingBillingAlert = false
})
},
async setBillingAlert() {
// Validate fields
const isValid = await this.$refs.billingAlertForm.validate();
if (!isValid) return;
this.isSendingBillingAlert = true
axios
.post('/api/subscriptions/billing-alerts', {
amount: this.billingAlertAmount
})
.then(() => {
this.$store.dispatch('getAppData')
events.$emit('toaster', {
type: 'success',
message: this.$t('Your billing alert was set successfully'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingBillingAlert = false
})
},
async makePayment() {
// Validate fields
const isValid = await this.$refs.fundAccount.validate();
if (!isValid) return;
// Show payment methods popup
this.$store.dispatch('callSingleChargeProcess', this.chargeAmount)
},
async storeStripePaymentMethod() {
this.stripeData.storingStripePaymentMethod = true
const {error} = await stripe.confirmSetup({
//`Elements` instance that was used to create the Payment Element
elements,
redirect: 'if_required',
confirmParams: {
return_url: window.location.href,
}
});
if (error) {
// This point will only be reached if there is an immediate error when
// confirming the payment. Show error to your customer (e.g., payment
// details incomplete)
const messageContainer = document.querySelector('#error-message');
messageContainer.textContent = error.message;
} else {
// Your customer will be redirected to your `return_url`. For some payment
// methods like iDEAL, your customer will be redirected to an intermediate
// site first to authorize the payment, then redirected to the `return_url`.
events.$emit('toaster', {
type: 'success',
message: this.$t('Your credit card was stored successfully'),
})
// TODO: L9 - load credit card after was stored in database
}
this.stripeData.storingStripePaymentMethod = false
},
async stripeInit() {
// Init stripe js
stripe = await loadStripe(this.config.stripe_public_key);
await axios.get('/api/stripe/setup-intent')
.then(response => {
// Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 2
elements = stripe.elements({
clientSecret: response.data.client_secret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: this.config.app_color,
fontFamily: 'Nunito',
borderRadius: '8px',
colorText: this.isDarkMode ? '#bec6cf' : '#1B2539',
colorBackground: this.isDarkMode ? '#191b1e' : '#fff',
fontWeightNormal: '700',
fontSizeSm: '0.875rem',
colorSuccessText: '#0ABB87',
colorSuccess: '#0ABB87',
colorWarning: '#fd397a',
colorWarningText: '#fd397a',
colorDangerText: '#fd397a',
colorTextSecondary: '#6b7280',
spacingGridRow: '20px',
}
},
});
// Create and mount the Payment Element
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
this.stripeData.isInitialization = false
},
deleteBillingAlert() {
events.$emit('confirm:open', {
title: this.$t('Are you sure you want to delete your alert?'),
message: this.$t('You will no longer receive any notifications that your billing limit has been exceeded.'),
action: {
id: this.user.data.relationships.alert.data.id,
operation: 'delete-billing-alert',
}
})
},
deleteCreditCard(id) {
events.$emit('confirm:open', {
title: this.$t('Are you sure you want to delete your credit card?'),
message: this.$t('We will no longer settle your payments automatically and you will have to fund your account for the next payments.'),
action: {
id: id,
operation: 'delete-credit-card',
}
})
},
showStoreCreditCardForm() {
this.isCreditCardForm = !this.isCreditCardForm
this.stripeInit()
}
},
created() {
events.$on('action:confirmed', data => {
if (data.operation === 'delete-billing-alert')
axios.delete(`/api/subscriptions/billing-alerts/${this.user.data.relationships.alert.data.id}`)
.then(() => {
this.$store.dispatch('getAppData')
this.showUpdateBillingAlertForm = false
this.billingAlertAmount = undefined
events.$emit('toaster', {
type: 'success',
message: this.$t('Your billing alert was deleted.'),
})
})
.catch(() => this.$isSomethingWrong())
if (data.operation === 'delete-credit-card')
axios.delete(`/api/stripe/credit-cards/${data.id}`)
.then(() => {
this.$store.dispatch('getAppData')
events.$emit('toaster', {
type: 'success',
message: this.$t('Your credit card was deleted.'),
})
})
.catch(() => this.$isSomethingWrong())
})
}
}
</script>

View File

@@ -1,276 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<PageTabGroup>
<!--Page title-->
<FormLabel>{{ $t('user_payments.title') }}</FormLabel>
<!--Add payment method button-->
<div class="page-actions" v-if="PaymentMethods && PaymentMethods.length > 0">
<router-link :to="{name: 'CreatePaymentMethod'}">
<MobileActionButton icon="credit-card">
{{ $t('user_payments.add_card') }}
</MobileActionButton>
</router-link>
</div>
<!--Payment methods table-->
<DatatableWrapper v-if="PaymentMethods" :table-data="{data: PaymentMethods}" :paginator="false" :columns="columns" class="table">
<!--Table data content-->
<template slot-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">{{ $t('global.default') }}</ColorLabel>
</div>
</span>
</td>
<td>
<span class="cell-item">
{{ row.data.attributes.exp_month }} / {{ row.data.attributes.exp_year }}
</span>
</td>
<td>
<div class="action-icons">
<label class="icon-wrapper" :title="$t('user_payments.set_as_default')">
<credit-card-icon size="15" class="icon icon-card" @click="setDefaultCard(row.data.attributes)" v-if="row.data.id !== defaultPaymentCard.data.id"></credit-card-icon>
</label>
<label class="icon-wrapper" :title="$t('user_payments.delete_card')">
<trash2-icon size="15" class="icon icon-trash" @click="deleteCard(row.data.attributes)"></trash2-icon>
</label>
</div>
</td>
</tr>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox>
<p>{{ $t('user_payments.empty') }} <router-link v-if="user.data.attributes.stripe_customer" :to="{name: 'CreatePaymentMethod'}" class="text-theme">Add new payment method.</router-link> </p>
</InfoBox>
</template>
</DatatableWrapper>
</PageTabGroup>
</PageTab>
</template>
<script>
import MobileActionButton from '/resources/js/components/FilesView/MobileActionButton'
import DatatableWrapper from '/resources/js/components/Others/Tables/DatatableWrapper'
import PageTabGroup from '/resources/js/components/Others/Layout/PageTabGroup'
import {CreditCardIcon, Trash2Icon} from "vue-feather-icons"
import FormLabel from '/resources/js/components/Others/Forms/FormLabel'
import PageTab from '/resources/js/components/Others/Layout/PageTab'
import ColorLabel from '/resources/js/components/Others/ColorLabel'
import InfoBox from '/resources/js/components/Others/Forms/InfoBox'
import { mapGetters } from 'vuex'
import {events} from '/resources/js/bus'
import axios from 'axios'
export default {
name: 'UserPaymentMethods',
components: {
MobileActionButton,
DatatableWrapper,
CreditCardIcon,
PageTabGroup,
Trash2Icon,
ColorLabel,
FormLabel,
InfoBox,
PageTab,
},
computed: {
...mapGetters(['user']),
},
data() {
return {
defaultPaymentCard: undefined,
PaymentMethods: undefined,
deletingID: undefined,
columns: [
{
label: this.$t('rows.card.number'),
field: 'data.attributes.total',
sortable: false
},
{
label: this.$t('rows.card.expiration'),
field: 'data.attributes.total',
sortable: false
},
{
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
}
},
setDefaultCard(card) {
events.$emit('confirm:open', {
title: this.$t('popup_set_card.title'),
message: this.$t('popup_set_card.message'),
buttonColor: 'theme-solid',
action: {
id: card.card_id,
operation: 'set-as-default-credit-card'
}
})
},
deleteCard(card) {
events.$emit('confirm:open', {
title: this.$t('popup_delete_card.title'),
message: this.$t('popup_delete_card.message'),
action: {
id: card.card_id,
operation: 'delete-credit-card'
}
})
},
fetchPaymentMethods() {
axios.get('/api/user/subscription/payment-cards')
.then(response => {
if (response.status == 204) {
this.PaymentMethods = {}
}
if (response.status == 200) {
this.defaultPaymentCard = response.data.default
this.PaymentMethods = response.data.others.data
this.PaymentMethods.push(response.data.default)
}
}).finally(() => {
this.isLoading = false
}
)
}
},
created() {
// Get payments card
this.fetchPaymentMethods()
// Delete credit card
events.$on('action:confirmed', data => {
if (data.operation === 'delete-credit-card') {
this.deletingID = data.id
axios.post('/api/user/subscription/payment-cards/' + data.id, {
_method: 'delete'
})
.then(() => {
// Get payments card
this.fetchPaymentMethods()
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.card_deleted'),
})
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
}
if (data.operation === 'set-as-default-credit-card') {
axios.post('/api/user/subscription/payment-cards/' + data.id, {
default: 1,
_method: 'patch'
})
.then(() => {
// Get payments card
this.fetchPaymentMethods()
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.card_set'),
})
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
}
})
},
destroyed() {
events.$off('action:confirmed')
}
}
</script>
<style lang="scss" scoped>
@import '/resources/sass/vuefilemanager/_variables';
@import '/resources/sass/vuefilemanager/_mixins';
@import '/resources/sass/vuefilemanager/_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;
}
}
.page-actions {
margin-top: 45px;
margin-bottom: 10px;
}
@media only screen and (max-width: 960px) {
}
.dark {
}
</style>

View File

@@ -1,299 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<div v-if="subscription" class="card shadow-card">
<FormLabel>
{{ $t('Subscription') }}
</FormLabel>
<b class="text-3xl font-extrabold -mt-3 block mb-0.5">
{{ status }}
</b>
<b class="mb-3 block text-sm text-gray-400 mb-8">
{{ subscription.relationships.plan.data.attributes.name }} / {{ price }}
</b>
<div v-for="(limit, i) in limitations" :key="i" :class="{'mb-6': (Object.keys(limitations).length - 1) !== i}">
<b class="mb-3 block text-sm text-gray-400">
{{ limit.message }}
</b>
<ProgressLine :data="limit.distribution" />
</div>
</div>
<div v-if="subscription && ['paystack', 'paypal'].includes(subscription.attributes.driver)" class="card shadow-card">
<FormLabel>
{{ $t('Update Payments') }}
</FormLabel>
<AppInputSwitch :title="$t('Update your Payment Method')" :description="$t('You will be redirected to your payment provider to edit your payment method.')" :is-last="true">
<ButtonBase @click.native="updatePaymentMethod" :loading="isGeneratedUpdateLink" class="popup-button" button-style="theme">
{{ $t('Update Payments') }}
</ButtonBase>
</AppInputSwitch>
</div>
<div v-if="subscription" class="card shadow-card">
<FormLabel>
{{ $t('Edit your Subscription') }}
</FormLabel>
<AppInputSwitch :title="$t('Cancel Subscription')" :description="$t('You can cancel your subscription now. You\'ll continue to have access to the features you\'ve paid for until the end of your billing cycle.')">
<ButtonBase @click.native="cancelSubscriptionConfirmation" :loading="isCancelling" class="popup-button" button-style="secondary">
{{ $t('Cancel Now') }}
</ButtonBase>
</AppInputSwitch>
<AppInputSwitch :title="$t('Upgrade Plan')" :description="$t('You can upgrade your plan at any time you want.')" :is-last="true">
<ButtonBase @click.native="$openUpgradeOptions" class="popup-button" button-style="secondary">
{{ $t('Upgrade Now') }}
</ButtonBase>
</AppInputSwitch>
</div>
<!--Transactions-->
<div class="card shadow-card">
<FormLabel icon="file-text">
{{ $t('Transactions') }}
</FormLabel>
<DatatableWrapper
@init="isLoading = false"
api="/api/subscriptions/transactions"
:paginator="true"
:columns="columns"
>
<template slot-scope="{ row }">
<tr class="border-b dark:border-opacity-5 border-light border-dashed">
<td class="py-4">
<span class="text-sm font-bold">
{{ row.data.attributes.note }}
</span>
</td>
<td>
<ColorLabel :color="$getTransactionStatusColor(row.data.attributes.status)">
{{ row.data.attributes.status }}
</ColorLabel>
</td>
<td>
<span class="text-sm font-bold" :class="$getTransactionTypeTextColor(row.data.attributes.type)">
{{ $getTransactionMark(row.data.attributes.type) + row.data.attributes.price }}
</span>
</td>
<td>
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="text-right">
<img class="inline-block max-h-5" :src="$getPaymentLogo(row.data.attributes.driver)" :alt="row.data.attributes.driver">
</td>
</tr>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox>
<p>{{ $t('admin_page_user.invoices.empty') }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</div>
<div v-if="! subscription && !isLoading" class="card shadow-card">
<InfoBox style="margin-bottom: 0">
<p>{{ $t("You don't have any subscription") }}</p>
</InfoBox>
</div>
</PageTab>
</template>
<script>
import DatatableWrapper from "../../components/Others/Tables/DatatableWrapper";
import ColorLabel from "../../components/Others/ColorLabel";
import ButtonBase from '/resources/js/components/FilesView/ButtonBase'
import PageTab from '/resources/js/components/Others/Layout/PageTab'
import InfoBox from '/resources/js/components/Others/Forms/InfoBox'
import AppInputSwitch from "../../components/Admin/AppInputSwitch"
import FormLabel from "../../components/Others/Forms/FormLabel"
import ProgressLine from "../../components/Admin/ProgressLine"
import {events} from '/resources/js/bus'
import {mapGetters} from 'vuex'
import axios from 'axios'
export default {
name: 'UserSubscription',
components: {
DatatableWrapper,
ColorLabel,
AppInputSwitch,
ProgressLine,
ButtonBase,
FormLabel,
InfoBox,
PageTab,
},
computed: {
...mapGetters([
'user',
]),
status() {
return {
'active': `Active until ${this.subscription.attributes.renews_at}`,
'cancelled': `Ends at ${this.subscription.attributes.ends_at}`,
}[this.subscription.attributes.status]
},
price() {
return {
'month': `${this.subscription.relationships.plan.data.attributes.price} Per Month`,
'year': `${this.subscription.relationships.plan.data.attributes.price} Per Year`,
}[this.subscription.relationships.plan.data.attributes.interval]
},
},
data() {
return {
isGeneratedUpdateLink: false,
subscription: undefined,
isCancelling: false,
limitations: [],
isLoading: true,
columns: [
{
label: this.$t('Note'),
field: 'note',
sortable: true
},
{
label: this.$t('Status'),
field: 'status',
sortable: true
},
{
label: this.$t('admin_page_invoices.table.total'),
field: 'amount',
sortable: true
},
{
label: this.$t('Payed At'),
field: 'created_at',
sortable: true
},
{
label: this.$t('Service'),
field: 'driver',
sortable: true
},
],
}
},
methods: {
updatePaymentMethod() {
this.isGeneratedUpdateLink = true
axios.post(`/api/subscriptions/edit/${this.subscription.id}`)
.then(response => {
window.location = response.data.url
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
this.isGeneratedUpdateLink = false
})
},
cancelSubscriptionConfirmation() {
events.$emit('confirm:open', {
title: this.$t('Are you sure you want to cancel subscription?'),
message: this.$t("You'll continue to have access to the features you've paid for until the end of your billing cycle."),
action: {
operation: 'cancel-subscription',
}
})
},
cancelSubscription() {
// Start deleting spinner button
this.isCancelling = true
// Send post request
axios
.post('/api/subscriptions/cancel')
.then(() => {
// Update user data
this.$store.dispatch('getAppData').then(() => {
this.fetchSubscriptionDetail()
})
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_subscription_cancel.title'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isCancelling = false
})
},
fetchSubscriptionDetail() {
axios.get('/api/subscriptions/detail')
.then(response => {
this.subscription = response.data.data
Object
.entries(this.user.data.meta.limitations)
.map(([key, item]) => {
let payload = {
color: {
'max_storage_amount': 'warning',
'max_team_members': 'purple',
},
message: {
'max_storage_amount': `Total ${item.use} of ${item.total} Used`,
'max_team_members': `Total ${item.use} of ${item.total} Members`,
},
title: {
'max_storage_amount': `Storage`,
'max_team_members': `Team Members`,
}
}
this.limitations.push({
message: payload.message[key],
distribution: [
{
progress: item.percentage,
color: payload.color[key],
title: payload.title[key],
}
]
})
})
this.isLoading = false
})
.finally(() => {
this.isLoading = false
})
}
},
created() {
this.fetchSubscriptionDetail()
events.$on('action:confirmed', data => {
if (data.operation === 'cancel-subscription')
this.cancelSubscription()
})
}
}
</script>