mirror of
https://github.com/VueFileManager/vuefilemanager.git
synced 2026-05-30 23:44:41 +00:00
v1.7 beta.8
This commit is contained in:
+1
-1
@@ -33,7 +33,7 @@ MAIL_USERNAME=
|
|||||||
MAIL_PASSWORD=
|
MAIL_PASSWORD=
|
||||||
MAIL_ENCRYPTION=
|
MAIL_ENCRYPTION=
|
||||||
MAIL_FROM_ADDRESS="${MAIL_USERNAME}"
|
MAIL_FROM_ADDRESS="${MAIL_USERNAME}"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${MAIL_USERNAME}"
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ class WebhookController extends CashierController
|
|||||||
*/
|
*/
|
||||||
public function handleCustomerSubscriptionDeleted($payload)
|
public function handleCustomerSubscriptionDeleted($payload)
|
||||||
{
|
{
|
||||||
|
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
|
||||||
|
$user->subscriptions->filter(function ($subscription) use ($payload) {
|
||||||
|
return $subscription->stripe_id === $payload['data']['object']['id'];
|
||||||
|
})->each(function ($subscription) {
|
||||||
|
$subscription->markAsCancelled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
$user = User::where('stripe_id', $payload['data']['object']['customer'])->firstOrFail();
|
$user = User::where('stripe_id', $payload['data']['object']['customer'])->firstOrFail();
|
||||||
|
|
||||||
@@ -29,4 +37,26 @@ class WebhookController extends CashierController
|
|||||||
|
|
||||||
return $this->successMethod();
|
return $this->successMethod();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Invoice Payment Succeeded
|
||||||
|
*
|
||||||
|
* @param $payload
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function handleInvoicePaymentSucceeded($payload)
|
||||||
|
{
|
||||||
|
// Get user
|
||||||
|
$user = User::where('stripe_id', $payload['data']['object']['customer'])->firstOrFail();
|
||||||
|
|
||||||
|
// Get requested plan
|
||||||
|
$plan = $this->stripe->getPlan($user->subscription('main')->stripe_plan);
|
||||||
|
|
||||||
|
// Update user storage limit
|
||||||
|
$user->settings()->update([
|
||||||
|
'storage_capacity' => $plan['product']['metadata']['capacity']
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->successMethod();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,17 @@ class UserResource extends JsonResource
|
|||||||
'id' => (string)$this->id,
|
'id' => (string)$this->id,
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
'storage_capacity' => $this->settings->storage_capacity,
|
'storage_capacity' => $this->settings->storage_capacity,
|
||||||
'subscription' => $this->subscribed('main'),
|
'subscription' => $this->subscribed('main'),
|
||||||
'stripe_customer' => is_null($this->stripe_id) ? false : true,
|
'incomplete_payment' => $this->hasIncompletePayment('main') ? route('cashier.payment', $this->subscription('main')->latestPayment()->id) : null,
|
||||||
'name' => $this->name,
|
'stripe_customer' => is_null($this->stripe_id) ? false : true,
|
||||||
'email' => env('APP_DEMO') ? obfuscate_email($this->email) : $this->email,
|
'name' => $this->name,
|
||||||
'avatar' => $this->avatar,
|
'email' => env('APP_DEMO') ? obfuscate_email($this->email) : $this->email,
|
||||||
'role' => $this->role,
|
'avatar' => $this->avatar,
|
||||||
'created_at_formatted' => format_date($this->created_at, '%d. %B. %Y'),
|
'role' => $this->role,
|
||||||
'created_at' => $this->created_at,
|
'created_at_formatted' => format_date($this->created_at, '%d. %B. %Y'),
|
||||||
'updated_at' => $this->updated_at,
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'relationships' => [
|
'relationships' => [
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ class UserSubscription extends JsonResource
|
|||||||
'id' => $subscription['plan']['id'],
|
'id' => $subscription['plan']['id'],
|
||||||
'type' => 'subscription',
|
'type' => 'subscription',
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
/*'is_highest' => is_highest_plan($this->plan),*/
|
'incomplete' => $this->subscription('main')->incomplete(),
|
||||||
'active' => $subscription['plan']['active'],
|
'active' => $this->subscription('main')->active(),
|
||||||
'canceled' => $this->subscription('main')->cancelled(),
|
'canceled' => $this->subscription('main')->cancelled(),
|
||||||
'name' => $subscription['product']['name'],
|
'name' => $subscription['product']['name'],
|
||||||
'capacity' => (int)$subscription['product']['metadata']['capacity'],
|
'capacity' => (int)$subscription['product']['metadata']['capacity'],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Artisan;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Cashier\Exceptions\IncompletePayment;
|
use Laravel\Cashier\Exceptions\IncompletePayment;
|
||||||
|
use Laravel\Cashier\Exceptions\PaymentActionRequired;
|
||||||
use Stripe;
|
use Stripe;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ class StripeService
|
|||||||
* @param $request
|
* @param $request
|
||||||
* @param $user
|
* @param $user
|
||||||
* @param $paymentMethod
|
* @param $paymentMethod
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
*/
|
*/
|
||||||
public function createOrReplaceSubscription($request, $user)
|
public function createOrReplaceSubscription($request, $user)
|
||||||
{
|
{
|
||||||
@@ -138,7 +140,15 @@ class StripeService
|
|||||||
|
|
||||||
} catch (IncompletePayment $exception) {
|
} catch (IncompletePayment $exception) {
|
||||||
|
|
||||||
throw new HttpException(400, 'We can\'t charge your card');
|
if ($exception instanceof PaymentActionRequired) {
|
||||||
|
|
||||||
|
$cashier_route = route('cashier.payment', [$exception->payment->id, 'redirect' => url('/settings/subscription')]);
|
||||||
|
|
||||||
|
throw new HttpException(402, $cashier_route);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(400, $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -23,7 +23,7 @@
|
|||||||
{{ $t('page_index.menu.log_in') }}
|
{{ $t('page_index.menu.log_in') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="config.userRegistration">
|
||||||
<router-link class="cta-button" :to="{name: 'SignUp'}">
|
<router-link class="cta-button" :to="{name: 'SignUp'}">
|
||||||
{{ $t('page_index.menu.sign_in') }}
|
{{ $t('page_index.menu.sign_in') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
p, a {
|
p, a {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-avatar" :class="size">
|
<div class="user-avatar" :class="size">
|
||||||
|
<span v-if="isIncompletePayment || isNearlyFullStorageCapacity" class="notification"></span>
|
||||||
<img :src="user.data.attributes.avatar" :alt="user.data.attributes.name">
|
<img :src="user.data.attributes.avatar" :alt="user.data.attributes.name">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,6 +15,12 @@
|
|||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['user']),
|
...mapGetters(['user']),
|
||||||
|
isIncompletePayment() {
|
||||||
|
return this.user.data.attributes.incomplete_payment
|
||||||
|
},
|
||||||
|
isNearlyFullStorageCapacity() {
|
||||||
|
return this.config.storageLimit && this.user.relationships.storage.data.attributes.used > 95
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -24,6 +31,22 @@
|
|||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
width: 40px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
background: $red;
|
||||||
|
border: 2px solid $light_background;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -32,6 +55,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
|
margin: 0;
|
||||||
|
width: 54px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
@@ -42,6 +67,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.user-avatar {
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
border-color: $dark_mode_foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -297,6 +297,7 @@
|
|||||||
"item_counts": "{count} 个文件 | {count} 个文件"
|
"item_counts": "{count} 个文件 | {count} 个文件"
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
|
"incomplete": "Incomplete",
|
||||||
"active": "活性",
|
"active": "活性",
|
||||||
"admin": "管理员",
|
"admin": "管理员",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
@@ -671,6 +672,10 @@
|
|||||||
"description": "You nearly reach your storage capacity.",
|
"description": "You nearly reach your storage capacity.",
|
||||||
"title": "You reach your storage capacity. Please upgrade."
|
"title": "You reach your storage capacity. Please upgrade."
|
||||||
},
|
},
|
||||||
|
"incomplete_payment": {
|
||||||
|
"description": "Your latest payment is incomplete. {0}",
|
||||||
|
"href": "Please confirm your payment."
|
||||||
|
},
|
||||||
"uploading": {
|
"uploading": {
|
||||||
"progress": "上传文件 {current}/{total}"
|
"progress": "上传文件 {current}/{total}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -297,6 +297,7 @@
|
|||||||
"item_counts": "{count} Item | {count} Items"
|
"item_counts": "{count} Item | {count} Items"
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
|
"incomplete": "Incomplete",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@@ -671,6 +672,10 @@
|
|||||||
"description": "You nearly reach your storage capacity.",
|
"description": "You nearly reach your storage capacity.",
|
||||||
"title": "You reach your storage capacity. Please upgrade."
|
"title": "You reach your storage capacity. Please upgrade."
|
||||||
},
|
},
|
||||||
|
"incomplete_payment": {
|
||||||
|
"description": "Your latest payment is incomplete. {0}",
|
||||||
|
"href": "Please confirm your payment."
|
||||||
|
},
|
||||||
"uploading": {
|
"uploading": {
|
||||||
"progress": "Uploading files {current}/{total}"
|
"progress": "Uploading files {current}/{total}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -297,6 +297,7 @@
|
|||||||
"item_counts": "{count} Položka | {count} Položky"
|
"item_counts": "{count} Položka | {count} Položky"
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
|
"incomplete": "Incomplete",
|
||||||
"active": "Aktívny",
|
"active": "Aktívny",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
@@ -671,6 +672,10 @@
|
|||||||
"description": "Takmer ste dosiahli kapacitu úložiska",
|
"description": "Takmer ste dosiahli kapacitu úložiska",
|
||||||
"title": "Dosiahli ste kapacitu úložiska. Prosím inovujte úložisko."
|
"title": "Dosiahli ste kapacitu úložiska. Prosím inovujte úložisko."
|
||||||
},
|
},
|
||||||
|
"incomplete_payment": {
|
||||||
|
"description": "Vaša posledná platba nie je dokončená. {0}",
|
||||||
|
"href": "Prosím potvrďte Vašu platbu."
|
||||||
|
},
|
||||||
"uploading": {
|
"uploading": {
|
||||||
"progress": "Nahrávam súbory {current}/{total}"
|
"progress": "Nahrávam súbory {current}/{total}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
<span class="email">{{ user.data.attributes.email }}</span>
|
<span class="email">{{ user.data.attributes.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="config.storageLimit && config.isSaaS && config.app_payments_active" class="headline-actions">
|
<div v-if="config.storageLimit && config.isSaaS && config.app_payments_active && !canShowIncompletePayment" class="headline-actions">
|
||||||
<router-link :to="{name: 'UpgradePlan'}" v-if="canShowUpgradeButton">
|
<router-link :to="{name: 'UpgradePlan'}" v-if="canShowUpgradeButton">
|
||||||
<ButtonBase class="upgrade-button" button-style="secondary" type="button">
|
<ButtonBase class="upgrade-button" button-style="secondary" type="button">
|
||||||
{{ $t('global.upgrade_plan') }}
|
{{ $t('global.upgrade_plan') }}
|
||||||
@@ -100,7 +100,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InfoBox v-if="canShowUpgradeWarning" type="error" class="upgrade-box">
|
<!--Incomplete Payment Warning-->
|
||||||
|
<InfoBox v-if="canShowIncompletePayment" type="error" class="message-box">
|
||||||
|
<i18n path="incomplete_payment.description" tag="p">
|
||||||
|
<a :href="user.data.attributes.incomplete_payment">{{ $t('incomplete_payment.href') }}</a>
|
||||||
|
</i18n>
|
||||||
|
</InfoBox>
|
||||||
|
|
||||||
|
<!--Upgrade Storage Plan Warning-->
|
||||||
|
<InfoBox v-if="canShowUpgradeWarning && !canShowIncompletePayment" type="error" class="message-box">
|
||||||
<p>{{ $t('upgrade_banner.title') }}</p>
|
<p>{{ $t('upgrade_banner.title') }}</p>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
|
||||||
@@ -171,6 +179,9 @@
|
|||||||
},
|
},
|
||||||
canShowUpgradeWarning() {
|
canShowUpgradeWarning() {
|
||||||
return this.config.storageLimit && this.user.relationships.storage.data.attributes.used > 95
|
return this.config.storageLimit && this.user.relationships.storage.data.attributes.used > 95
|
||||||
|
},
|
||||||
|
canShowIncompletePayment() {
|
||||||
|
return this.user.data.attributes.incomplete_payment
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -226,8 +237,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-box {
|
.message-box {
|
||||||
margin-top: -30px;
|
margin-top: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|||||||
@@ -305,9 +305,6 @@
|
|||||||
// Update user data
|
// Update user data
|
||||||
this.$store.dispatch('getAppData')
|
this.$store.dispatch('getAppData')
|
||||||
|
|
||||||
// End loading
|
|
||||||
this.isSubmitted = false
|
|
||||||
|
|
||||||
// Show toaster
|
// Show toaster
|
||||||
events.$emit('toaster', {
|
events.$emit('toaster', {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -319,13 +316,16 @@
|
|||||||
},
|
},
|
||||||
errorOrder(error) {
|
errorOrder(error) {
|
||||||
|
|
||||||
if (error.response.status = 402) {
|
// Redirect user to confirmation payment page
|
||||||
|
if (error.response.status === 402) {
|
||||||
|
window.location.href = error.response.data.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show user error message
|
||||||
|
if (error.response.status === 400) {
|
||||||
this.isError = true
|
this.isError = true
|
||||||
this.errorMessage = error.response.data.message
|
this.errorMessage = error.response.data.message
|
||||||
}
|
}
|
||||||
|
|
||||||
// End loading
|
|
||||||
this.isSubmitted = false
|
|
||||||
},
|
},
|
||||||
async submitOrder() {
|
async submitOrder() {
|
||||||
|
|
||||||
@@ -375,6 +375,9 @@
|
|||||||
})
|
})
|
||||||
.then(() => this.successOrder())
|
.then(() => this.successOrder())
|
||||||
.catch((error) => this.errorOrder(error))
|
.catch((error) => this.errorOrder(error))
|
||||||
|
.finally(() => {
|
||||||
|
this.isSubmitted = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +394,9 @@
|
|||||||
})
|
})
|
||||||
.then(() => this.successOrder())
|
.then(() => this.successOrder())
|
||||||
.catch((error) => this.errorOrder(error))
|
.catch((error) => this.errorOrder(error))
|
||||||
|
.finally(() => {
|
||||||
|
this.isSubmitted = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
<div id="single-page">
|
<div id="single-page">
|
||||||
<div id="page-content" class="large-width center-page" v-show="! isLoading">
|
<div id="page-content" class="large-width center-page" v-show="! isLoading">
|
||||||
<MobileHeader :title="$router.currentRoute.meta.title"/>
|
<MobileHeader :title="$router.currentRoute.meta.title"/>
|
||||||
|
|
||||||
<div class="content-page">
|
<div class="content-page">
|
||||||
|
|
||||||
|
<!--Page Title-->
|
||||||
<div class="plan-title">
|
<div class="plan-title">
|
||||||
<cloud-icon size="42" class="title-icon"></cloud-icon>
|
<cloud-icon size="42" class="title-icon"></cloud-icon>
|
||||||
<h1>{{ $t('page_pricing_tables.title') }}</h1>
|
<h1>{{ $t('page_pricing_tables.title') }}</h1>
|
||||||
<h2>{{ $t('page_pricing_tables.description') }}</h2>
|
<h2>{{ $t('page_pricing_tables.description') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--Pricing Tables-->
|
||||||
<PlanPricingTables @load="onLoadPricingTables" @selected-plan="onSelectTable"/>
|
<PlanPricingTables @load="onLoadPricingTables" @selected-plan="onSelectTable"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +57,10 @@
|
|||||||
StripeElementsScript.setAttribute('src', 'https://js.stripe.com/v3/')
|
StripeElementsScript.setAttribute('src', 'https://js.stripe.com/v3/')
|
||||||
document.head.appendChild(StripeElementsScript)
|
document.head.appendChild(StripeElementsScript)
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
// Reload user data
|
||||||
|
this.$store.dispatch('getAppData')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@
|
|||||||
return this.isConfirmedResume ? 'theme-solid' : 'secondary'
|
return this.isConfirmedResume ? 'theme-solid' : 'secondary'
|
||||||
},
|
},
|
||||||
status() {
|
status() {
|
||||||
|
if (this.subscription.data.attributes.incomplete) {
|
||||||
|
return this.$t('global.incomplete')
|
||||||
|
}
|
||||||
if (this.subscription.data.attributes.canceled) {
|
if (this.subscription.data.attributes.canceled) {
|
||||||
return this.$t('global.canceled')
|
return this.$t('global.canceled')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,11 @@
|
|||||||
<div id="invoice-wrapper">
|
<div id="invoice-wrapper">
|
||||||
<header class="invoice-header">
|
<header class="invoice-header">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/assets/images/vuefilemanager-horizontal-logo.svg" alt="VueFileManager">
|
@if(isset($settings->app_logo_horizontal))
|
||||||
|
<img src="{{ url($settings->app_logo_horizontal) }}" alt="{{ $settings->app_title ?? 'VueFileManager' }}">
|
||||||
|
@else
|
||||||
|
<h1>{{ $settings->app_title ?? 'VueFileManager' }}</h1>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>@lang('vuefilemanager.invoice_title')</h1>
|
<h1>@lang('vuefilemanager.invoice_title')</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user