diff --git a/packages/bbui/src/Banner/BannerDisplay.svelte b/packages/bbui/src/Banner/BannerDisplay.svelte
index aad742b1bd..4785fcb9ba 100644
--- a/packages/bbui/src/Banner/BannerDisplay.svelte
+++ b/packages/bbui/src/Banner/BannerDisplay.svelte
@@ -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"
- {#if $banner.message}
+ {#each $banner.messages as message}
{
+ message.onChange()
+ }}
+ showCloseButton={typeof message.showCloseButton === "boolean"
+ ? message.showCloseButton
+ : true}
>
- {$banner.message}
+
+ {message.message}
+
- {/if}
+ {/each}
diff --git a/packages/bbui/src/Stores/banner.js b/packages/bbui/src/Stores/banner.js
index 81a9ee2204..745c77e188 100644
--- a/packages/bbui/src/Stores/banner.js
+++ b/packages/bbui/src/Stores/banner.js
@@ -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,
}
}
diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte
index 92f5c6f474..0c6c8e167b 100644
--- a/packages/bbui/src/Tooltip/TooltipWrapper.svelte
+++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte
@@ -54,7 +54,6 @@
transform: scale(0.75);
}
.icon-small {
- margin-top: -2px;
- margin-bottom: -5px;
+ margin-bottom: -2px;
}
diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js
index b45f3e9ed6..11424b1261 100644
--- a/packages/bbui/src/index.js
+++ b/packages/bbui/src/index.js
@@ -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"
diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte
index 0fb0fe59d5..4d193df104 100644
--- a/packages/builder/src/App.svelte
+++ b/packages/builder/src/App.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 }
@@ -12,6 +13,9 @@
+
+
+
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 35c4587874..69bca7eac3 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -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)
diff --git a/packages/builder/src/builderStore/store/temporal.js b/packages/builder/src/builderStore/store/temporal.js
new file mode 100644
index 0000000000..b8a75e1905
--- /dev/null
+++ b/packages/builder/src/builderStore/store/temporal.js
@@ -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 },
+ }
+}
diff --git a/packages/builder/src/components/common/TemplateDisplay.svelte b/packages/builder/src/components/common/TemplateDisplay.svelte
index c96bff8d5e..6041a904fd 100644
--- a/packages/builder/src/components/common/TemplateDisplay.svelte
+++ b/packages/builder/src/components/common/TemplateDisplay.svelte
@@ -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}
>
-
+ {#if $licensing?.usageMetrics?.apps < 100}
+
+ {/if}
+ 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()
+ }
+
+
+
+ {
+ window.location.href = upgradeUrl
+ }
+ : null}
+ >
+
+ The payment for your Business Subscription failed and we have downgraded
+ your account to the Free plan.
+
+
+ Update to Business to get all your apps and user sessions back up and
+ running.
+
+ {#if !$auth.user.accountPortalAccess}
+ Please contact the account holder.
+ {/if}
+
+
+
+
diff --git a/packages/builder/src/components/portal/licensing/AppLimitModal.svelte b/packages/builder/src/components/portal/licensing/AppLimitModal.svelte
new file mode 100644
index 0000000000..45259edd76
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/AppLimitModal.svelte
@@ -0,0 +1,46 @@
+
+
+
+ {
+ window.location.href = upgradeUrl
+ }
+ : null}
+ >
+
+ You are currently on our Free plan. Upgrade
+ to our Pro plan to get unlimited apps.
+
+ {#if !$auth.user.accountPortalAccess}
+ Please contact the account holder.
+ {/if}
+
+
+
+
diff --git a/packages/builder/src/components/portal/licensing/DayPassWarningModal.svelte b/packages/builder/src/components/portal/licensing/DayPassWarningModal.svelte
new file mode 100644
index 0000000000..f164df1105
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/DayPassWarningModal.svelte
@@ -0,0 +1,54 @@
+
+
+
+ {#if $auth.user.accountPortalAccess}
+ {
+ window.location.href = upgradeUrl
+ }}
+ >
+
+ You have used {sessionsUsed}% of
+ your plans Day Passes with {daysRemaining} day{daysRemaining == 1
+ ? ""
+ : "s"} remaining.
+
+ Upgrade your account to prevent your apps from going offline.
+
+ {:else}
+
+
+ You have used {sessionsUsed}% of
+ your plans Day Passes with {daysRemaining} day{daysRemaining == 1
+ ? ""
+ : "s"} remaining.
+
+ Please contact your account holder.
+
+ {/if}
+
diff --git a/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte b/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte
new file mode 100644
index 0000000000..2a744179c0
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/LicensingOverlays.svelte
@@ -0,0 +1,117 @@
+
+
+
+
+
diff --git a/packages/builder/src/components/portal/licensing/PaymentFailedModal.svelte b/packages/builder/src/components/portal/licensing/PaymentFailedModal.svelte
new file mode 100644
index 0000000000..f952684998
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/PaymentFailedModal.svelte
@@ -0,0 +1,87 @@
+
+
+
+ {#if $auth.user.accountPortalAccess}
+ {
+ window.location.href = upgradeUrl
+ }}
+ >
+ The payment for your business plan subscription has failed
+
+ Please upgrade your billing details before your account gets downgraded
+ to the free plan
+
+
+
+ {`${$licensing.paymentDueDaysRemaining} day${
+ $licensing.paymentDueDaysRemaining == 1 ? "" : "s"
+ } remaining`}
+
+
+
+
+
+
+ {:else}
+
+ The payment for your business plan subscription has failed
+
+ Please upgrade your billing details before your account gets downgraded
+ to the free plan
+
+ Please contact your account holder.
+
+
+ {`${$licensing.paymentDueDaysRemaining} day${
+ $licensing.paymentDueDaysRemaining == 1 ? "" : "s"
+ } remaining`}
+
+
+
+
+
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/components/portal/licensing/banners.js b/packages/builder/src/components/portal/licensing/banners.js
new file mode 100644
index 0000000000..3f8a2fbb0f
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/banners.js
@@ -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()
+ )
+ })
+}
diff --git a/packages/builder/src/components/portal/licensing/constants.js b/packages/builder/src/components/portal/licensing/constants.js
new file mode 100644
index 0000000000..6474b7c78f
--- /dev/null
+++ b/packages/builder/src/components/portal/licensing/constants.js
@@ -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",
+}
diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte
index 2e8ea2ef0a..ee8b1bb8df 100644
--- a/packages/builder/src/pages/builder/_layout.svelte
+++ b/packages/builder/src/pages/builder/_layout.svelte
@@ -1,6 +1,6 @@
@@ -121,6 +133,7 @@
>
+