mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
add trial banners and unblock member invite modal during trial
This commit is contained in:
committed by
Constantin Graf
parent
2bad9eaa3c
commit
30ed47d3fb
@@ -26,22 +26,14 @@ watchEffect(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-if="show && message"
|
v-if="show && message"
|
||||||
:class="{
|
class="bg-secondary border-b border-border-secondary">
|
||||||
'bg-indigo-500': style == 'success',
|
<div class="mx-auto py-1 px-3 sm:px-6 lg:px-8">
|
||||||
'bg-red-700': style == 'danger',
|
|
||||||
}">
|
|
||||||
<div class="max-w-screen-xl mx-auto py-2 px-3 sm:px-6 lg:px-8">
|
|
||||||
<div class="flex items-center justify-between flex-wrap">
|
<div class="flex items-center justify-between flex-wrap">
|
||||||
<div class="w-0 flex-1 flex items-center min-w-0">
|
<div class="w-0 flex-1 flex items-center min-w-0">
|
||||||
<span
|
<span class="flex">
|
||||||
class="flex p-2 rounded-lg"
|
|
||||||
:class="{
|
|
||||||
'bg-indigo-600': style == 'success',
|
|
||||||
'bg-red-600': style == 'danger',
|
|
||||||
}">
|
|
||||||
<svg
|
<svg
|
||||||
v-if="style == 'success'"
|
v-if="style == 'success'"
|
||||||
class="h-5 w-5 text-white"
|
class="h-6 w-6 text-text-secondary"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -76,13 +68,7 @@ watchEffect(async () => {
|
|||||||
<div class="shrink-0 sm:ms-3">
|
<div class="shrink-0 sm:ms-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="-me-1 flex p-2 rounded-md focus:outline-none sm:-me-2 transition"
|
class="-me-1 flex p-2 rounded-md focus:outline-none sm:-me-2 transition hover:bg-tertiary focus:bg-tertiary"
|
||||||
:class="{
|
|
||||||
'hover:bg-indigo-600 focus:bg-indigo-600':
|
|
||||||
style == 'success',
|
|
||||||
'hover:bg-red-600 focus:bg-red-600':
|
|
||||||
style == 'danger',
|
|
||||||
}"
|
|
||||||
aria-label="Dismiss"
|
aria-label="Dismiss"
|
||||||
@click.prevent="show = false">
|
@click.prevent="show = false">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
120
resources/js/Components/Billing/BillingBanner.vue
Normal file
120
resources/js/Components/Billing/BillingBanner.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
||||||
|
import {
|
||||||
|
CheckBadgeIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from '@heroicons/vue/16/solid';
|
||||||
|
import { Link } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { isBlocked, isFreePlan, isInTrial } from '@/utils/billing';
|
||||||
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
|
|
||||||
|
const hideTrialBanner = useSessionStorage(
|
||||||
|
'showTrialBanner-' + getCurrentOrganizationId(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const showTrialBanner = computed(() => isInTrial() && !hideTrialBanner.value);
|
||||||
|
const hideBlockedBanner = useSessionStorage(
|
||||||
|
'showBlockedBanner-' + getCurrentOrganizationId(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const showBlockedBanner = computed(
|
||||||
|
() => isBlocked() && !hideBlockedBanner.value
|
||||||
|
);
|
||||||
|
const hideFreeUpgradeBanner = useSessionStorage(
|
||||||
|
'showFreeUpgradeBanner-' + getCurrentOrganizationId(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const showFreeUpgradeBanner = computed(
|
||||||
|
() => isFreePlan() && !isBlocked() && !hideFreeUpgradeBanner.value
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showTrialBanner"
|
||||||
|
class="bg-accent-600/50 text-sm py-0.5 border-b border-border-secondary">
|
||||||
|
<MainContainer class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<CheckBadgeIcon class="w-4 text-white/50"></CheckBadgeIcon>
|
||||||
|
<span class="font-medium"> Your trial expires in X days. </span>
|
||||||
|
<span>
|
||||||
|
To continue using all features & support the development of
|
||||||
|
solidtime, please upgrade your plan.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Link href="/billing">
|
||||||
|
<div
|
||||||
|
class="text-white font-semibold uppercase text-xs flex space-x-1 items-center hover:bg-white/10 rounded-lg px-2 py-1.5">
|
||||||
|
<span>Upgrade now</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<button @click="showTrialBanner = false" class="p-1">
|
||||||
|
<XMarkIcon
|
||||||
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</MainContainer>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showBlockedBanner"
|
||||||
|
class="bg-red-600/50 text-sm py-0.5 border-b border-border-secondary">
|
||||||
|
<MainContainer class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<XCircleIcon class="w-4 text-white/50"></XCircleIcon>
|
||||||
|
<span class="font-medium">
|
||||||
|
Your organization is currently blocked.
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Please upgrade to a premium plan or remove all users except
|
||||||
|
the owner to unblock your organization.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Link href="/billing">
|
||||||
|
<div
|
||||||
|
class="text-white font-semibold uppercase text-xs flex space-x-1 items-center hover:bg-white/10 rounded-lg px-2 py-1.5">
|
||||||
|
<span>Upgrade now</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<button @click="showBlockedBanner = false" class="p-1">
|
||||||
|
<XMarkIcon
|
||||||
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</MainContainer>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showFreeUpgradeBanner"
|
||||||
|
class="bg-tertiary text-sm py-0.5 border-b border-border-secondary">
|
||||||
|
<MainContainer class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-1.5">
|
||||||
|
<XCircleIcon class="w-4 text-white/50"></XCircleIcon>
|
||||||
|
<span class="font-medium">
|
||||||
|
Your are currently using the Free Plan.
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
>To unlock all premium features & support the development of
|
||||||
|
solidtime, please upgrade your plan.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Link href="/billing">
|
||||||
|
<div
|
||||||
|
class="text-white font-semibold uppercase text-xs flex space-x-1 items-center hover:bg-white/10 rounded-lg px-2 py-1.5">
|
||||||
|
<span>Upgrade now</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<button @click="showFreeUpgradeBanner = false" class="p-1">
|
||||||
|
<XMarkIcon
|
||||||
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</MainContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||||
@@ -11,7 +11,11 @@ import type { Role } from '@/types/jetstream';
|
|||||||
import { Link, useForm } from '@inertiajs/vue3';
|
import { Link, useForm } from '@inertiajs/vue3';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { filterRoles } from '@/utils/roles';
|
import { filterRoles } from '@/utils/roles';
|
||||||
import { hasActiveSubscription, isBillingActivated } from '@/utils/billing';
|
import {
|
||||||
|
hasActiveSubscription,
|
||||||
|
isBillingActivated,
|
||||||
|
isInTrial,
|
||||||
|
} from '@/utils/billing';
|
||||||
import { CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
import { CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||||
import { canUpdateOrganization } from '@/utils/permissions';
|
import { canUpdateOrganization } from '@/utils/permissions';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
@@ -80,6 +84,14 @@ async function submit() {
|
|||||||
|
|
||||||
const clientNameInput = ref<HTMLInputElement | null>(null);
|
const clientNameInput = ref<HTMLInputElement | null>(null);
|
||||||
useFocus(clientNameInput, { initialValue: true });
|
useFocus(clientNameInput, { initialValue: true });
|
||||||
|
|
||||||
|
const inviteMembersIsAllowed = computed(() => {
|
||||||
|
return (
|
||||||
|
!isBillingActivated() ||
|
||||||
|
(isBillingActivated() && hasActiveSubscription()) ||
|
||||||
|
(isBillingActivated() && isInTrial())
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -91,7 +103,7 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-if="isBillingActivated() && !hasActiveSubscription()">
|
<div v-if="!inviteMembersIsAllowed">
|
||||||
<div
|
<div
|
||||||
class="rounded-full flex items-center justify-center w-20 h-20 mx-auto border border-border-tertiary bg-secondary">
|
class="rounded-full flex items-center justify-center w-20 h-20 mx-auto border border-border-tertiary bg-secondary">
|
||||||
<UserGroupIcon class="w-12"></UserGroupIcon>
|
<UserGroupIcon class="w-12"></UserGroupIcon>
|
||||||
@@ -201,7 +213,7 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
v-if="!isBillingActivated() || hasActiveSubscription()"
|
v-if="inviteMembersIsAllowed"
|
||||||
class="ms-3"
|
class="ms-3"
|
||||||
:class="{ 'opacity-25': saving }"
|
:class="{ 'opacity-25': saving }"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type { User } from '@/types/models';
|
|||||||
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
|
import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid';
|
||||||
import { fetchToken, isTokenValid } from '@/utils/session';
|
import { fetchToken, isTokenValid } from '@/utils/session';
|
||||||
import UpdateSidebarNotification from '@/Components/UpdateSidebarNotification.vue';
|
import UpdateSidebarNotification from '@/Components/UpdateSidebarNotification.vue';
|
||||||
|
import BillingBanner from '@/Components/Billing/BillingBanner.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
@@ -217,6 +218,7 @@ const page = usePage<{
|
|||||||
<Head :title="title" />
|
<Head :title="title" />
|
||||||
|
|
||||||
<Banner />
|
<Banner />
|
||||||
|
<BillingBanner v-if="isBillingActivated()" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="min-h-screen bg-default-background border-l border-default-background-separator">
|
class="min-h-screen bg-default-background border-l border-default-background-separator">
|
||||||
|
|||||||
@@ -8,6 +8,29 @@ export function isBillingActivated() {
|
|||||||
return page.props.has_billing_extension;
|
return page.props.has_billing_extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInTrial() {
|
||||||
|
const page = usePage<{
|
||||||
|
billing: {
|
||||||
|
has_trial: boolean;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
return page.props.billing.has_trial;
|
||||||
|
}
|
||||||
|
export function isBlocked() {
|
||||||
|
const page = usePage<{
|
||||||
|
billing: {
|
||||||
|
is_blocked: boolean;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
return page.props.billing.is_blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFreePlan() {
|
||||||
|
return !hasActiveSubscription() && !isInTrial();
|
||||||
|
}
|
||||||
|
|
||||||
export function hasActiveSubscription() {
|
export function hasActiveSubscription() {
|
||||||
const page = usePage<{
|
const page = usePage<{
|
||||||
billing: {
|
billing: {
|
||||||
|
|||||||
Reference in New Issue
Block a user