add trial banners and unblock member invite modal during trial

This commit is contained in:
Gregor Vostrak
2024-09-04 15:24:31 +02:00
committed by Constantin Graf
parent 2bad9eaa3c
commit 30ed47d3fb
5 changed files with 166 additions and 23 deletions

View File

@@ -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

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

View File

@@ -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"

View File

@@ -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">

View File

@@ -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: {