Merge commit
This commit is contained in:
parent
1a6d50008e
commit
60feccaac0
|
@ -4,22 +4,30 @@
|
|||
import { banner } from "../Stores/banner"
|
||||
import Banner from "./Banner.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
</script>
|
||||
|
||||
<Portal target=".banner-container">
|
||||
<div class="banner">
|
||||
{#if $banner.message}
|
||||
{#each $banner.messages as message}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Banner
|
||||
type={$banner.type}
|
||||
extraButtonText={$banner.extraButtonText}
|
||||
extraButtonAction={$banner.extraButtonAction}
|
||||
on:change={$banner.onChange}
|
||||
type={message.type}
|
||||
extraButtonText={message.extraButtonText}
|
||||
extraButtonAction={message.extraButtonAction}
|
||||
on:change={() => {
|
||||
message.onChange()
|
||||
}}
|
||||
showCloseButton={typeof message.showCloseButton === "boolean"
|
||||
? message.showCloseButton
|
||||
: true}
|
||||
>
|
||||
{$banner.message}
|
||||
<TooltipWrapper tooltip={"test"}>
|
||||
{message.message}
|
||||
</TooltipWrapper>
|
||||
</Banner>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export function createBannerStore() {
|
||||
const DEFAULT_CONFIG = {}
|
||||
const DEFAULT_CONFIG = {
|
||||
messages: [],
|
||||
}
|
||||
|
||||
const banner = writable(DEFAULT_CONFIG)
|
||||
|
||||
|
@ -28,9 +30,23 @@ export function createBannerStore() {
|
|||
await show(config)
|
||||
}
|
||||
|
||||
const queue = async entries => {
|
||||
banner.update(store => {
|
||||
const sorted = [...store.messages, ...entries].sort(
|
||||
(a, b) => a.priority > b.priority
|
||||
)
|
||||
return {
|
||||
...store,
|
||||
messages: sorted,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: banner.subscribe,
|
||||
showStatus,
|
||||
show,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
|||
export { default as Page } from "./Layout/Page.svelte"
|
||||
export { default as Link } from "./Link/Link.svelte"
|
||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||
export { default as Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||
import { parse, stringify } from "qs"
|
||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
|
||||
|
||||
const queryHandler = { parse, stringify }
|
||||
</script>
|
||||
|
@ -12,6 +13,9 @@
|
|||
<BannerDisplay />
|
||||
|
||||
<NotificationDisplay />
|
||||
|
||||
<LicensingOverlays />
|
||||
|
||||
<Router {routes} config={{ queryHandler }} />
|
||||
<div class="modal-container" />
|
||||
<HelpIcon />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
|
||||
export const selectedScreen = derived(store, $store => {
|
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const getTemporalStore = () => {
|
||||
const initialValue = {}
|
||||
|
||||
//const appId = window["##BUDIBASE_APP_ID##"] || "app"
|
||||
const localStorageKey = `${123}.bb-temporal`
|
||||
const store = createLocalStorageStore(localStorageKey, initialValue)
|
||||
|
||||
const setExpiring = (key, data, duration) => {
|
||||
const updated = {
|
||||
...data,
|
||||
expiry: Date.now() + duration * 1000,
|
||||
}
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
[key]: updated,
|
||||
}))
|
||||
}
|
||||
|
||||
const getExpiring = key => {
|
||||
const entry = get(store)[key]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
const currentExpiry = entry.expiry
|
||||
if (currentExpiry < Date.now()) {
|
||||
store.update(state => {
|
||||
delete state[key]
|
||||
return state
|
||||
})
|
||||
return null
|
||||
} else {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { setExpiring, getExpiring },
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let templates
|
||||
|
||||
|
@ -96,15 +97,17 @@
|
|||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{#if $licensing?.usageMetrics?.apps < 100}
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{/if}
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let accountDowngradeModal
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
accountDowngradeModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
accountDowngradeModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={accountDowngradeModal} on:show={onShow} on:hide={onDismiss}>
|
||||
<ModalContent
|
||||
title="Your account is now on the Free plan"
|
||||
size="M"
|
||||
showCancelButton={$auth.user.accountPortalAccess}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||
onConfirm={$auth.user.accountPortalAccess
|
||||
? () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
: null}
|
||||
>
|
||||
<Body>
|
||||
The payment for your Business Subscription failed and we have downgraded
|
||||
your account to the <span class="free-plan">Free plan</span>.
|
||||
</Body>
|
||||
<Body>
|
||||
Update to Business to get all your apps and user sessions back up and
|
||||
running.
|
||||
</Body>
|
||||
{#if !$auth.user.accountPortalAccess}
|
||||
<Body>Please contact the account holder.</Body>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.free-plan {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
|
||||
let appLimitModal
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
appLimitModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
appLimitModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={appLimitModal} on:hide={onDismiss}>
|
||||
<ModalContent
|
||||
title="Upgrade to get more apps"
|
||||
size="M"
|
||||
showCancelButton={false}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||
onConfirm={$auth.user.accountPortalAccess
|
||||
? () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
: null}
|
||||
>
|
||||
<Body>
|
||||
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
|
||||
to our Pro plan to get unlimited apps.
|
||||
</Body>
|
||||
{#if !$auth.user.accountPortalAccess}
|
||||
<Body>Please contact the account holder.</Body>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.free-plan {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
import { licensing, auth, admin } from "stores/portal"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let sessionsModal
|
||||
|
||||
const outOfSessionsTitle = "You are almost out of sessions"
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
$: daysRemaining = $licensing.quotaResetDaysRemaining
|
||||
$: sessionsUsed = $licensing.usageMetrics?.dayPasses
|
||||
|
||||
export function show() {
|
||||
sessionsModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
sessionsModal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={sessionsModal} on:show={onShow} on:hide={onDismiss}>
|
||||
{#if $auth.user.accountPortalAccess}
|
||||
<ModalContent
|
||||
title={outOfSessionsTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
}}
|
||||
>
|
||||
<Body>
|
||||
You have used <span class="session_percent">{sessionsUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
</Body>
|
||||
<Body>Upgrade your account to prevent your apps from going offline.</Body>
|
||||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent title={outOfSessionsTitle} size="M" showCancelButton={false}>
|
||||
<Body>
|
||||
You have used <span class="session_percent">{sessionsUsed}%</span> of
|
||||
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||
? ""
|
||||
: "s"} remaining.
|
||||
</Body>
|
||||
<Body>Please contact your account holder.</Body>
|
||||
</ModalContent>
|
||||
{/if}
|
||||
</Modal>
|
|
@ -0,0 +1,117 @@
|
|||
<script>
|
||||
import { licensing, auth } from "stores/portal"
|
||||
import { temporalStore } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import DayPassWarningModal from "./DayPassWarningModal.svelte"
|
||||
import PaymentFailedModal from "./PaymentFailedModal.svelte"
|
||||
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
|
||||
import { ExpiringKeys } from "./constants"
|
||||
import { getBanners } from "./banners"
|
||||
import { banner } from "@budibase/bbui"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
let queuedBanners = []
|
||||
let queuedModals = []
|
||||
let dayPassModal
|
||||
let paymentFailedModal
|
||||
let accountDowngradeModal
|
||||
let userLoaded = false
|
||||
let loaded = false
|
||||
let licensingLoaded = false
|
||||
let currentModalCfg = null
|
||||
|
||||
const processModals = () => {
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const dismissableModals = [
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL,
|
||||
criteria: () => {
|
||||
return $licensing?.usageMetrics?.dayPasses >= 90
|
||||
},
|
||||
action: () => {
|
||||
dayPassModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
|
||||
criteria: () => {
|
||||
return $licensing.accountPastDue
|
||||
},
|
||||
action: () => {
|
||||
paymentFailedModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_PAYMENT_FAILED)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL,
|
||||
criteria: () => {
|
||||
return $licensing?.accountDowngraded
|
||||
},
|
||||
action: () => {
|
||||
accountDowngradeModal.show()
|
||||
},
|
||||
cache: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL)
|
||||
},
|
||||
},
|
||||
]
|
||||
return dismissableModals.filter(modal => {
|
||||
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
|
||||
})
|
||||
}
|
||||
|
||||
$: if (userLoaded && licensingLoaded && loaded) {
|
||||
queuedModals = processModals()
|
||||
queuedBanners = getBanners()
|
||||
showNext()
|
||||
banner.queue(queuedBanners)
|
||||
}
|
||||
|
||||
const showNext = () => {
|
||||
if (currentModalCfg) {
|
||||
currentModalCfg.cache()
|
||||
}
|
||||
if (queuedModals.length) {
|
||||
currentModalCfg = queuedModals.shift()
|
||||
currentModalCfg.action()
|
||||
} else {
|
||||
currentModalCfg = null
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
auth.subscribe(state => {
|
||||
if (state.user && !userLoaded) {
|
||||
userLoaded = true
|
||||
}
|
||||
})
|
||||
|
||||
licensing.subscribe(state => {
|
||||
if (state.usageMetrics && !licensingLoaded) {
|
||||
licensingLoaded = true
|
||||
}
|
||||
})
|
||||
|
||||
temporalStore.subscribe(state => {
|
||||
console.log("Stored temporal ", state)
|
||||
})
|
||||
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNext} />
|
||||
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNext} />
|
||||
<AccountDowngradedModal
|
||||
bind:this={accountDowngradeModal}
|
||||
onDismiss={showNext}
|
||||
/>
|
|
@ -0,0 +1,87 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||
import { auth, admin, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let onDismiss = () => {}
|
||||
export let onShow = () => {}
|
||||
|
||||
let paymentFailedModal
|
||||
let pastDueAt
|
||||
|
||||
const paymentFailedTitle = "Payment failed"
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
|
||||
export function show() {
|
||||
paymentFailedModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
paymentFailedModal.hide()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
auth.subscribe(state => {
|
||||
if (state.user && state.user.license?.billing?.subscription) {
|
||||
pastDueAt = new Date(
|
||||
state.user.license?.billing?.subscription.pastDueAt * 1000
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:this={paymentFailedModal} on:show={onShow} on:hide={onDismiss}>
|
||||
{#if $auth.user.accountPortalAccess}
|
||||
<ModalContent
|
||||
title={paymentFailedTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
}}
|
||||
>
|
||||
<Body>The payment for your business plan subscription has failed</Body>
|
||||
<Body>
|
||||
Please upgrade your billing details before your account gets downgraded
|
||||
to the free plan
|
||||
</Body>
|
||||
<Body weight={800}>
|
||||
<div class="tooltip-root">
|
||||
{`${$licensing.paymentDueDaysRemaining} day${
|
||||
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
|
||||
} remaining`}
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
|
||||
</span>
|
||||
</div>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent title={paymentFailedTitle} size="M" showCancelButton={false}>
|
||||
<Body>The payment for your business plan subscription has failed</Body>
|
||||
<Body>
|
||||
Please upgrade your billing details before your account gets downgraded
|
||||
to the free plan
|
||||
</Body>
|
||||
<Body>Please contact your account holder.</Body>
|
||||
<Body weight={800}>
|
||||
<div class="tooltip-root">
|
||||
{`${$licensing.paymentDueDaysRemaining} day${
|
||||
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
|
||||
} remaining`}
|
||||
<span class="tooltip">
|
||||
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
|
||||
</span>
|
||||
</div>
|
||||
</Body>
|
||||
</ModalContent>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.tooltip-root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,132 @@
|
|||
import { ExpiringKeys } from "./constants"
|
||||
import { temporalStore } from "builderStore"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
const upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const defaultAction = key => {
|
||||
if (!get(auth).user.accountPortalAccess) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
extraButtonText: "Upgrade Plan",
|
||||
extraButtonAction: () => {
|
||||
defaultCacheFn(key)
|
||||
window.location.href = upgradeUrl
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const buildUsageInfoBanner = (metricKey, metricLabel, cacheKey, percentage) => {
|
||||
const appAuth = get(auth)
|
||||
const appLicensing = get(licensing)
|
||||
|
||||
let bannerConfig = {
|
||||
key: cacheKey,
|
||||
type: "info",
|
||||
onChange: () => {
|
||||
defaultCacheFn(cacheKey)
|
||||
},
|
||||
message: `You have used ${
|
||||
appLicensing?.usageMetrics[metricKey]
|
||||
}% of your monthly usage of ${metricLabel} with ${
|
||||
appLicensing.quotaResetDaysRemaining
|
||||
} day${
|
||||
appLicensing.quotaResetDaysRemaining == 1 ? "" : "s"
|
||||
} remaining. All apps will be taken offline if this limit is reached. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder."
|
||||
}`,
|
||||
criteria: () => {
|
||||
return appLicensing?.usageMetrics[metricKey] >= percentage
|
||||
},
|
||||
priority: 0, //Banners.Priority 0, 1, 2 ??
|
||||
}
|
||||
|
||||
return !get(auth).user.accountPortalAccess
|
||||
? bannerConfig
|
||||
: {
|
||||
...bannerConfig,
|
||||
...defaultAction(cacheKey),
|
||||
}
|
||||
}
|
||||
|
||||
const buildDayPassBanner = () => {
|
||||
const appAuth = get(auth)
|
||||
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
|
||||
return {
|
||||
key: "max_dayPasses",
|
||||
type: "negative",
|
||||
criteria: () => {
|
||||
return true
|
||||
},
|
||||
message: `Your apps are currently offline. You have exceeded your plans limit for Day Passes. ${
|
||||
appAuth.user.accountPortalAccess
|
||||
? ""
|
||||
: "Please contact your account holder."
|
||||
}`,
|
||||
...defaultAction(),
|
||||
showCloseButton: false,
|
||||
}
|
||||
}
|
||||
|
||||
return buildUsageInfoBanner(
|
||||
"dayPasses",
|
||||
"Day Passes",
|
||||
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
|
||||
90
|
||||
)
|
||||
}
|
||||
|
||||
const buildPaymentFailedBanner = () => {
|
||||
return {
|
||||
key: "payment_Failed",
|
||||
type: "negative",
|
||||
criteria: () => {
|
||||
return get(licensing)?.accountPastDue
|
||||
},
|
||||
message: `Payment Failed - Please update your billing details or your account will be downgrades in
|
||||
${get(licensing)?.paymentDueDaysRemaining} day${
|
||||
get(licensing)?.paymentDueDaysRemaining == 1 ? "" : "s"
|
||||
}`,
|
||||
...defaultAction(),
|
||||
showCloseButton: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const getBanners = () => {
|
||||
return [
|
||||
buildPaymentFailedBanner(),
|
||||
buildDayPassBanner(ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER),
|
||||
buildUsageInfoBanner(
|
||||
"rows",
|
||||
"Rows",
|
||||
ExpiringKeys.LICENSING_ROWS_WARNING_BANNER,
|
||||
90
|
||||
),
|
||||
buildUsageInfoBanner(
|
||||
"automations",
|
||||
"Automations",
|
||||
ExpiringKeys.LICENSING_AUTOMATIONS_WARNING_BANNER,
|
||||
90
|
||||
),
|
||||
buildUsageInfoBanner(
|
||||
"queries",
|
||||
"Queries",
|
||||
ExpiringKeys.LICENSING_QUERIES_WARNING_BANNER,
|
||||
90 // could be an array [50,75,90]
|
||||
),
|
||||
].filter(licensingBanner => {
|
||||
return (
|
||||
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
||||
licensingBanner.criteria()
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export const ExpiringKeys = {
|
||||
LICENSING_DAYPASS_WARNING_MODAL: "licensing_daypass_warning_90_modal",
|
||||
LICENSING_DAYPASS_WARNING_BANNER: "licensing_daypass_warning_90_banner",
|
||||
LICENSING_PAYMENT_FAILED: "licensing_payment_failed",
|
||||
LICENSING_ACCOUNT_DOWNGRADED_MODAL: "licensing_account_downgraded_modal",
|
||||
LICENSING_APP_LIMIT_MODAL: "licensing_app_limit_modal",
|
||||
LICENSING_ROWS_WARNING_BANNER: "licensing_rows_warning_banner",
|
||||
LICENSING_AUTOMATIONS_WARNING_BANNER: "licensing_automations_warning_banner",
|
||||
LICENSING_QUERIES_WARNING_BANNER: "licensing_automations_warning_banner",
|
||||
}
|
||||
|
||||
export const StripeStatus = {
|
||||
PAST_DUE: "past_due",
|
||||
ACTIVE: "active",
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { isActive, redirect, params } from "@roxi/routify"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
@ -63,6 +63,9 @@
|
|||
await auth.getSelf()
|
||||
await admin.init()
|
||||
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
|
||||
// Set init info if present
|
||||
if ($params["?template"]) {
|
||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||
|
|
|
@ -13,12 +13,14 @@
|
|||
} from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { templates } from "stores/portal"
|
||||
import { templates, licensing } from "stores/portal"
|
||||
|
||||
let loaded = $templates?.length
|
||||
let template
|
||||
let creationModal = false
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
|
||||
const welcomeBody =
|
||||
|
@ -29,6 +31,8 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
await templates.load()
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -41,9 +45,13 @@
|
|||
})
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
|
||||
const stopAppCreation = () => {
|
||||
|
@ -52,9 +60,13 @@
|
|||
}
|
||||
|
||||
const initiateAppImport = () => {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -121,6 +133,7 @@
|
|||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.title .welcome > .buttons {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
|
@ -34,6 +35,7 @@
|
|||
let creationModal
|
||||
let updatingModal
|
||||
let exportModal
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
let loaded = $apps?.length || $templates?.length
|
||||
let searchTerm = ""
|
||||
|
@ -126,8 +128,10 @@
|
|||
return `${app.name} - Automation error (${errorCount(errors)})`
|
||||
}
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
if ($apps?.length) {
|
||||
const initiateAppCreation = async () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else if ($apps?.length) {
|
||||
$goto("/builder/portal/apps/create")
|
||||
} else {
|
||||
template = null
|
||||
|
@ -227,6 +231,10 @@
|
|||
try {
|
||||
await apps.load()
|
||||
await templates.load()
|
||||
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -243,10 +251,6 @@
|
|||
notifications.error("Error loading apps and templates")
|
||||
}
|
||||
loaded = true
|
||||
|
||||
//Testing
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getTestData()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -415,6 +419,8 @@
|
|||
<ExportAppModal app={selectedApp} />
|
||||
</Modal>
|
||||
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.appTable {
|
||||
border-top: var(--border-light);
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { auth } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "components/portal/licensing/constants"
|
||||
|
||||
export const createLicensingStore = () => {
|
||||
const DEFAULT = {
|
||||
plans: {},
|
||||
}
|
||||
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
const store = writable(DEFAULT)
|
||||
|
||||
const actions = {
|
||||
|
@ -19,65 +23,86 @@ export const createLicensingStore = () => {
|
|||
}
|
||||
})
|
||||
},
|
||||
getTestData: async () => {
|
||||
const tenantId = get(auth).tenantId
|
||||
console.log("Tenant ", tenantId)
|
||||
|
||||
const license = get(auth).user.license
|
||||
console.log("User LICENSE ", license)
|
||||
|
||||
// Pull out the usage.
|
||||
getUsageMetrics: async () => {
|
||||
const quota = get(store).quotaUsage
|
||||
console.log("Quota usage", quota)
|
||||
const license = get(auth).user.license
|
||||
const now = Date.now()
|
||||
const nowSeconds = now / 1000
|
||||
|
||||
// Would only initialise the usage elements if the account element is present.
|
||||
console.log("User account ", get(auth).user.account)
|
||||
const getMetrics = (keys, license, quota) => {
|
||||
if (!license || !quota || !keys) {
|
||||
return {}
|
||||
}
|
||||
return keys.reduce((acc, key) => {
|
||||
const quotaLimit = license[key].value
|
||||
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||
|
||||
//separate into functions that pass in both the usage and quota
|
||||
//easier to test
|
||||
// Catch for sessions
|
||||
key = key === "sessions" ? "dayPasses" : key
|
||||
|
||||
const getMonthlyMetrics = (license, quota) => {
|
||||
return ["sessions", "queries", "automations"].reduce((acc, key) => {
|
||||
const quotaLimit = license.quotas.usage.monthly[key].value
|
||||
acc[key] =
|
||||
quotaLimit > -1
|
||||
? (quota.monthly.current[key] / quotaLimit) * 100
|
||||
: -1
|
||||
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
const monthlyMetrics = getMetrics(
|
||||
["sessions", "queries", "automations"],
|
||||
license.quotas.usage.monthly,
|
||||
quota.monthly.current
|
||||
)
|
||||
const staticMetrics = getMetrics(
|
||||
["apps", "rows"],
|
||||
license.quotas.usage.static,
|
||||
quota.usageQuota
|
||||
)
|
||||
|
||||
const getStaticMetrics = (license, quota) => {
|
||||
return ["apps", "rows"].reduce((acc, key) => {
|
||||
const quotaLimit = license.quotas.usage.monthly[key].value
|
||||
acc[key] =
|
||||
quotaLimit > -1 ? (quota.usageQuota[key] / quotaLimit) * 100 : -1
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const modQuotaStr = JSON.stringify(quota)
|
||||
const cloneQuota = JSON.parse(modQuotaStr)
|
||||
cloneQuota.monthly.current.sessions = 4
|
||||
|
||||
const monthlyMetrics = getMonthlyMetrics(license, cloneQuota)
|
||||
const staticMetrics = getStaticMetrics(license, cloneQuota)
|
||||
|
||||
console.log("Monthly Usage Metrics ", monthlyMetrics)
|
||||
console.log("Static Usage Metrics ", staticMetrics)
|
||||
|
||||
const flagged = Object.keys(monthlyMetrics).filter(key => {
|
||||
return monthlyMetrics[key] >= 100
|
||||
// DEBUG
|
||||
console.log("Store licensing val ", {
|
||||
...monthlyMetrics,
|
||||
...staticMetrics,
|
||||
})
|
||||
|
||||
console.log(flagged)
|
||||
let subscriptionDaysRemaining
|
||||
if (license?.billing?.subscription) {
|
||||
const currentPeriodEnd = license.billing.subscription.currentPeriodEnd
|
||||
const currentPeriodEndMilliseconds = currentPeriodEnd * 1000
|
||||
|
||||
// store.update(state => {
|
||||
// return {
|
||||
// ...state,
|
||||
// metrics,
|
||||
// }
|
||||
// })
|
||||
subscriptionDaysRemaining = Math.round(
|
||||
(currentPeriodEndMilliseconds - now) / oneDayInMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
const quotaResetDaysRemaining =
|
||||
quota.quotaReset > now
|
||||
? Math.round((quota.quotaReset - now) / oneDayInMilliseconds)
|
||||
: 0
|
||||
|
||||
const accountDowngraded =
|
||||
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||
license?.plan === Constants.PlanType.FREE
|
||||
|
||||
const accountPastDue =
|
||||
nowSeconds >= license?.billing?.subscription?.currentPeriodEnd &&
|
||||
nowSeconds <= license?.billing?.subscription?.pastDueAt &&
|
||||
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||
!accountDowngraded
|
||||
|
||||
const pastDueAtSeconds = license?.billing?.subscription?.pastDueAt
|
||||
const pastDueAtMilliseconds = pastDueAtSeconds * 1000
|
||||
const paymentDueDaysRemaining = Math.round(
|
||||
(pastDueAtMilliseconds - now) / oneDayInMilliseconds
|
||||
)
|
||||
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||
subscriptionDaysRemaining,
|
||||
paymentDueDaysRemaining,
|
||||
quotaResetDaysRemaining,
|
||||
accountDowngraded,
|
||||
accountPastDue,
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue