satellite/{web,console}: enable/disable billing features depending on config value

Added client side logic to disable billing features depending on config value.
Disabled billing endpoints if billing is disabled.

Issue:
https://github.com/storj/storj-private/issues/464

Change-Id: I6e70dc5e2372953b613ddab9f19cb94f008935ce
This commit is contained in:
Vitalii 2023-10-16 14:55:21 +03:00 committed by Storj Robot
parent bce022ea7a
commit 6ae28e2306
14 changed files with 191 additions and 100 deletions

View File

@ -336,6 +336,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
abRouter.Handle("/hit/{action}", http.HandlerFunc(abController.SendHit)).Methods(http.MethodPost, http.MethodOptions)
}
if config.BillingFeaturesEnabled {
paymentController := consoleapi.NewPayments(logger, service, accountFreezeService, packagePlans)
paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
paymentsRouter.Use(server.withCORS)
@ -360,6 +361,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
paymentsRouter.HandleFunc("/purchase-package", paymentController.PurchasePackage).Methods(http.MethodPost, http.MethodOptions)
paymentsRouter.HandleFunc("/package-available", paymentController.PackageAvailable).Methods(http.MethodGet, http.MethodOptions)
}
}
bucketsController := consoleapi.NewBuckets(logger, service)
bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()

View File

@ -9,7 +9,7 @@
<ProjectIcon />
<h1 class="modal__header__title">Get more projects</h1>
</div>
<p v-if="!isPaidTier" class="modal__info">
<p v-if="!isPaidTier && billingEnabled" class="modal__info">
Upgrade to Pro Account to create more projects and gain access to higher limits.
</p>
<p v-else class="modal__info">
@ -45,6 +45,7 @@ import { computed } from 'vue';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useConfigStore } from '@/store/modules/configStore';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
@ -53,14 +54,20 @@ import ProjectIcon from '@/../static/images/common/blueBox.svg';
const appStore = useAppStore();
const userStore = useUsersStore();
const configStore = useConfigStore();
const isPaidTier = computed<boolean>(() => userStore.state.user.paidTier);
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Returns button text label depending on if the user is in the free or paid tier.
*/
const buttonLabel = computed<string>(() => {
return isPaidTier.value ? 'Request -->' : 'Upgrade -->';
return !isPaidTier.value && billingEnabled.value ? 'Upgrade -->' : 'Request -->';
});
/**
@ -69,7 +76,7 @@ const buttonLabel = computed<string>(() => {
* Redirects to upgrade modal or opens new tab to request increase project limits .
*/
function onClick(): void {
if (!isPaidTier.value) {
if (!isPaidTier.value && billingEnabled.value) {
appStore.updateActiveModal(MODALS.upgradeAccount);
} else {
appStore.removeActiveModal();

View File

@ -8,8 +8,10 @@
<AccountIcon class="account-area__wrap__left__icon" />
<p class="account-area__wrap__left__label">My Account</p>
<p class="account-area__wrap__left__label-small">Account</p>
<TierBadgePro v-if="user.paidTier" class="account-area__wrap__left__tier-badge" />
<template v-if="billingEnabled">
<TierBadgePro v-if="isPaidTier" class="account-area__wrap__left__tier-badge" />
<TierBadgeFree v-else class="account-area__wrap__left__tier-badge" />
</template>
</div>
<ArrowImage class="account-area__wrap__arrow" />
</div>
@ -31,11 +33,11 @@
</a>
</div>
</div>
<div v-if="!user.paidTier" tabindex="0" class="account-area__dropdown__item" @click="onUpgrade" @keyup.enter="onUpgrade">
<div v-if="!isPaidTier && billingEnabled" tabindex="0" class="account-area__dropdown__item" @click="onUpgrade" @keyup.enter="onUpgrade">
<UpgradeIcon />
<p class="account-area__dropdown__item__label">Upgrade</p>
</div>
<div tabindex="0" class="account-area__dropdown__item" @click="navigateToBilling" @keyup.enter="navigateToBilling">
<div v-if="billingEnabled" tabindex="0" class="account-area__dropdown__item" @click="navigateToBilling" @keyup.enter="navigateToBilling">
<BillingIcon />
<p class="account-area__dropdown__item__label">Billing</p>
</div>
@ -55,7 +57,6 @@
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { User } from '@/types/users';
import { RouteConfig } from '@/types/router';
import { AuthHttpApi } from '@/api/auth';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
@ -108,6 +109,11 @@ const dropdownYPos = ref<number>(0);
const dropdownXPos = ref<number>(0);
const accountArea = ref<HTMLDivElement>();
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Returns bottom and left position of dropdown.
*/
@ -130,10 +136,10 @@ const satellite = computed((): string => {
});
/**
* Returns user entity from store.
* Indicates if user is in paid tier.
*/
const user = computed((): User => {
return usersStore.state.user;
const isPaidTier = computed((): boolean => {
return usersStore.state.user.paidTier;
});
/**
@ -142,6 +148,8 @@ const user = computed((): User => {
function onUpgrade(): void {
closeDropdown();
if (!billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}
@ -204,12 +212,15 @@ function toggleDropdown(): void {
}
const DROPDOWN_HEIGHT = 224; // pixels
const SIXTEEN_PIXELS = 16;
const FIFTY_THREE_PIXELS = 53; // height of a single child element.
const TWENTY_PIXELS = 20;
const SEVENTY_PIXELS = 70;
const accountContainer = accountArea.value.getBoundingClientRect();
dropdownYPos.value = accountContainer.bottom - DROPDOWN_HEIGHT - (usersStore.state.user.paidTier ? SIXTEEN_PIXELS : SEVENTY_PIXELS);
if (billingEnabled.value) {
dropdownYPos.value = accountContainer.bottom - DROPDOWN_HEIGHT - (isPaidTier.value ? 0 : FIFTY_THREE_PIXELS);
} else {
dropdownYPos.value = accountContainer.bottom - DROPDOWN_HEIGHT + FIFTY_THREE_PIXELS;
}
dropdownXPos.value = accountContainer.right - TWENTY_PIXELS;
appStore.toggleActiveDropdown(APP_STATE_DROPDOWNS.ACCOUNT);

View File

@ -157,8 +157,10 @@
<AccountIcon class="account-area__wrap__left__icon" />
<p class="account-area__wrap__left__label">My Account</p>
<p class="account-area__wrap__left__label-small">Account</p>
<template v-if="billingEnabled">
<TierBadgePro v-if="user.paidTier" class="account-area__wrap__left__tier-badge" />
<TierBadgeFree v-else class="account-area__wrap__left__tier-badge" />
</template>
</div>
<ArrowIcon class="account-area__wrap__arrow" />
</div>
@ -180,11 +182,11 @@
</a>
</div>
</div>
<div v-if="!user.paidTier" tabindex="0" class="account-area__dropdown__item" @click="onUpgrade" @keyup.enter="onUpgrade">
<div v-if="!user.paidTier && billingEnabled" tabindex="0" class="account-area__dropdown__item" @click="onUpgrade" @keyup.enter="onUpgrade">
<UpgradeIcon />
<p class="account-area__dropdown__item__label">Upgrade</p>
</div>
<div class="account-area__dropdown__item" @click="navigateToBilling">
<div v-if="billingEnabled" class="account-area__dropdown__item" @click="navigateToBilling">
<BillingIcon />
<p class="account-area__dropdown__item__label">Billing</p>
</div>
@ -294,7 +296,12 @@ const isAccountDropdownShown = ref<boolean>(false);
const isOpened = ref<boolean>(false);
const isLoading = ref<boolean>(false);
/*
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Whether the user is the owner of the selected project.
*/
const isProjectOwner = computed((): boolean => {
@ -525,6 +532,9 @@ function onCreateLinkClick(): void {
*/
function onUpgrade(): void {
isOpened.value = false;
if (!billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}
@ -893,7 +903,7 @@ async function onLogout(): Promise<void> {
&__wrap {
box-sizing: border-box;
padding: 16px 32px;
padding: 16px 32px 16px 36px;
height: 48px;
width: 100%;
display: flex;
@ -930,7 +940,7 @@ async function onLogout(): Promise<void> {
&__header {
background: var(--c-grey-1);
padding: 16px 32px;
padding: 16px 32px 16px 36px;
border: 1px solid var(--c-grey-2);
display: flex;
align-items: center;
@ -964,7 +974,7 @@ async function onLogout(): Promise<void> {
&__item {
display: flex;
align-items: center;
padding: 16px 32px;
padding: 16px 32px 16px 36px;
background: var(--c-grey-1);
&__label {

View File

@ -73,10 +73,11 @@ function closePicker(): void {
<style scoped lang="scss">
.range-selection {
background-color: #fff;
background-color: var(--c-white);
cursor: pointer;
font-family: 'font_regular', sans-serif;
position: relative;
border-radius: 8px;
&__toggle-container {
display: flex;

View File

@ -46,7 +46,7 @@
'https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212'"
/>
<LimitCard
v-if="coupon && isFreeTierCoupon"
v-if="coupon && isFreeTierCoupon && billingEnabled"
:icon="CheckmarkIcon"
title="Free Tier"
color="#091c45"
@ -62,7 +62,7 @@
link="https://docs.storj.io/dcs/pricing#free-tier"
/>
<LimitCard
v-if="coupon && !isFreeTierCoupon"
v-if="coupon && !isFreeTierCoupon && billingEnabled"
:icon="CheckmarkIcon"
title="Coupon"
color="#091c45"
@ -95,6 +95,7 @@ import { Coupon, ProjectCharges } from '@/types/payments';
import { centsToDollars } from '@/utils/strings';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { RouteConfig } from '@/types/router';
import { useConfigStore } from '@/store/modules/configStore';
import LimitCard from '@/components/project/dashboard/LimitCard.vue';
@ -107,6 +108,8 @@ const appStore = useAppStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const billingStore = useBillingStore();
const configStore = useConfigStore();
const router = useRouter();
const props = defineProps<{
@ -116,6 +119,11 @@ const props = defineProps<{
const EIGHTY_PERCENT = 80;
const HUNDRED_PERCENT = 100;
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Returns coupon from store.
*/
@ -305,6 +313,8 @@ function usageAction(limit: LimitToChange): void {
* Starts upgrade account flow.
*/
function startUpgradeFlow(): void {
if (!billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}

View File

@ -22,7 +22,7 @@
:toggle="toggleChartsDatePicker"
/>
<VButton
v-if="!isProAccount"
v-if="!isProAccount && billingEnabled"
label="Upgrade Plan"
width="114px"
height="40px"
@ -145,6 +145,7 @@
</template>
</InfoContainer>
<InfoContainer
v-if="billingEnabled"
:icon="BillingIcon"
title="Billing"
:subtitle="status"
@ -191,7 +192,8 @@ import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { centsToDollars } from '@/utils/strings';
import { User } from '@/types/users';
import { ProjectRole } from '@/types/projectMembers';
import { ProjectMembersPage, ProjectRole } from '@/types/projectMembers';
import { AccessGrantsPage } from '@/types/accessGrants';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useCreateProjectClickHandler } from '@/composables/useCreateProjectClickHandler';
@ -237,6 +239,11 @@ const isServerSideEncryptionBannerHidden = ref<boolean>(true);
const chartWidth = ref<number>(0);
const chartContainer = ref<HTMLDivElement>();
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Indicates if charts date picker is shown.
*/
@ -393,6 +400,8 @@ function getDimension(dataStamps: DataStamp[]): Dimensions {
* Holds on upgrade button click logic.
*/
function onUpgradeClick(): void {
if (!billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}
@ -472,21 +481,26 @@ onMounted(async (): Promise<void> => {
appStore.toggleHasJustLoggedIn();
}
await Promise.all([
const promises: Promise<void | ProjectMembersPage | AccessGrantsPage>[] = [
projectsStore.getDailyProjectData({ since: past, before: now }),
billingStore.getCoupon(),
pmStore.getProjectMembers(FIRST_PAGE, projectID),
agStore.getAccessGrants(FIRST_PAGE, projectID),
]);
];
if (billingEnabled.value) promises.push(billingStore.getCoupon());
await Promise.all(promises);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
} finally {
isDataFetching.value = false;
}
if (billingEnabled.value) {
billingStore.getProjectUsageAndChargesCurrentRollup().catch(error => {
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
});
}
try {
await bucketsStore.getBuckets(FIRST_PAGE, projectID);
@ -656,8 +670,7 @@ onBeforeUnmount((): void => {
&__info {
display: flex;
margin-top: 16px;
justify-content: space-between;
align-items: stretch;
column-gap: 10px;
flex-wrap: wrap;
.info-container {

View File

@ -388,6 +388,12 @@ router.beforeEach(async (to, from, next) => {
appStore.toggleHasJustLoggedIn(false);
}
if (!configStore.state.config.billingFeaturesEnabled && to.path.includes(RouteConfig.Billing.path)) {
next(RouteConfig.Account.with(RouteConfig.Settings).path);
return;
}
if (navigateToDefaultSubTab(to.matched, RouteConfig.Account)) {
next(RouteConfig.Account.with(RouteConfig.Billing).path);

View File

@ -73,6 +73,11 @@ const projectsStore = useProjectsStore();
// Minimum number of recovery codes before the recovery code warning bar is shown.
const recoveryCodeWarningThreshold = 4;
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
const isMyProjectsPage = computed((): boolean => {
return route.path === RouteConfig.AllProjectsDashboard.path;
});
@ -135,6 +140,7 @@ onMounted(async () => {
notify.error(`Unable to set access grants wizard. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
if (billingEnabled.value) {
try {
const couponType = await billingStore.setupAccount();
if (couponType === CouponType.NoCoupon) {
@ -148,6 +154,7 @@ onMounted(async () => {
error.message = `Unable to setup account. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
}
try {
await projectsStore.getUserInvitations();

View File

@ -10,7 +10,7 @@
/>
<v-banner
v-if="isAccountFrozen && parentRef"
v-if="isAccountFrozen && parentRef && billingEnabled"
class="all-dashboard-banners__freeze"
title="Your account was frozen due to billing issues."
message="Please update your payment information."
@ -21,7 +21,7 @@
/>
<v-banner
v-if="isAccountWarned && parentRef"
v-if="isAccountWarned && parentRef && billingEnabled"
class="all-dashboard-banners__warning"
title="Your account will be frozen soon due to billing issues."
message="Please update your payment information."
@ -52,6 +52,7 @@ import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { RouteConfig } from '@/types/router';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import VBanner from '@/components/common/VBanner.vue';
import UpgradeNotification from '@/components/notifications/UpgradeNotification.vue';
@ -61,11 +62,17 @@ const router = useRouter();
const billingStore = useBillingStore();
const usersStore = useUsersStore();
const appStore = useAppStore();
const configStore = useConfigStore();
const props = defineProps<{
parentRef: HTMLElement;
}>();
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Indicates if account was frozen due to billing issues.
*/
@ -92,7 +99,8 @@ const isLowBalance = computed((): boolean => {
/* whether the paid tier banner should be shown */
const isPaidTierBannerShown = computed((): boolean => {
return !usersStore.state.user.paidTier
&& joinedWhileAgo.value;
&& joinedWhileAgo.value
&& billingEnabled.value;
});
/* whether the user joined more than 7 days ago */
@ -107,7 +115,7 @@ const joinedWhileAgo = computed((): boolean => {
* Opens add payment method modal.
*/
function togglePMModal(): void {
if (usersStore.state.user.paidTier) return;
if (usersStore.state.user.paidTier || !billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}

View File

@ -69,8 +69,10 @@
<AccountIcon class="account-area__wrap__left__icon" />
<p class="account-area__wrap__left__label">My Account</p>
<p class="account-area__wrap__left__label-small">Account</p>
<template v-if="billingEnabled">
<TierBadgePro v-if="user.paidTier" class="account-area__wrap__left__tier-badge" />
<TierBadgeFree v-else class="account-area__wrap__left__tier-badge" />
</template>
</div>
<ArrowIcon class="account-area__wrap__arrow" />
</div>
@ -92,7 +94,7 @@
</a>
</div>
</div>
<div class="account-area__dropdown__item" @click="navigateToBilling">
<div v-if="billingEnabled" class="account-area__dropdown__item" @click="navigateToBilling">
<BillingIcon />
<p class="account-area__dropdown__item__label">Billing</p>
</div>
@ -177,6 +179,11 @@ const link = 'https://docs.storj.io/';
const isAccountDropdownShown = ref(false);
const isNavOpened = ref(false);
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Returns satellite name from store.
*/

View File

@ -32,7 +32,7 @@
</a>
</div>
</div>
<div tabindex="0" class="account-button__dropdown__item" @click.stop="navigateToBilling" @keyup.enter="navigateToBilling">
<div v-if="billingEnabled" tabindex="0" class="account-button__dropdown__item" @click.stop="navigateToBilling" @keyup.enter="navigateToBilling">
<BillingIcon />
<p class="account-button__dropdown__item__label">Billing</p>
</div>
@ -102,6 +102,11 @@ const obStore = useObjectBrowserStore();
const isHoveredOver = ref(false);
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Indicates if account dropdown is open.
*/

View File

@ -26,7 +26,7 @@
/>
<v-banner
v-if="isAccountFrozen && !isLoading && dashboardContent"
v-if="isAccountFrozen && !isLoading && dashboardContent && billingEnabled"
title="Your account was frozen due to billing issues."
message="Please update your payment information."
link-text="To Billing Page"
@ -36,7 +36,7 @@
/>
<v-banner
v-if="isAccountWarned && !isLoading && dashboardContent"
v-if="isAccountWarned && !isLoading && dashboardContent && billingEnabled"
title="Your account will be frozen soon due to billing issues."
message="Please update your payment information."
link-text="To Billing Page"
@ -46,7 +46,7 @@
/>
<limit-warning-banners
v-if="dashboardContent"
v-if="dashboardContent && billingEnabled"
:reached-thresholds="reachedThresholds"
:dashboard-ref="dashboardContent"
:on-upgrade-click="togglePMModal"
@ -72,7 +72,7 @@
<p>Remaining session time: <b class="dashboard__debug-timer__bold">{{ session.debugTimerText.value }}</b></p>
</div>
<limit-warning-modal
v-if="limitModalThreshold && !isLoading"
v-if="limitModalThreshold && !isLoading && billingEnabled"
:reached-thresholds="reachedThresholds"
:threshold="limitModalThreshold"
:on-close="() => limitModalThreshold = null"
@ -146,6 +146,11 @@ const limitModalThreshold = ref<LimitThreshold | null>(null);
const dashboardContent = ref<HTMLElement | null>(null);
/**
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Indicates whether objects upload modal should be shown.
*/
@ -236,7 +241,8 @@ const isPaidTierBannerShown = computed((): boolean => {
return !isPaidTier.value
&& !isOnboardingTour.value
&& joinedWhileAgo.value
&& isDashboardPage.value;
&& isDashboardPage.value
&& billingEnabled.value;
});
/* whether the user joined more than 7 days ago */
@ -304,13 +310,6 @@ const isPaidTier = computed((): boolean => {
return usersStore.state.user.paidTier;
});
/**
* Returns the URL for the general request page from the store.
*/
const requestURL = computed((): string => {
return configStore.state.config.generalRequestURL;
});
/**
* Closes upload large files warning notification.
*/
@ -356,7 +355,7 @@ function toggleMFARecoveryModal(): void {
* Opens add payment method modal.
*/
function togglePMModal(): void {
if (isPaidTier.value) return;
if (isPaidTier.value || !billingEnabled.value) return;
appStore.updateActiveModal(MODALS.upgradeAccount);
}
@ -404,6 +403,7 @@ onMounted(async () => {
notify.error(`Unable to set access grants wizard. ${error.message}`, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
}
if (billingEnabled.value) {
try {
const couponType = await billingStore.setupAccount();
if (couponType === CouponType.NoCoupon) {
@ -417,6 +417,7 @@ onMounted(async () => {
error.message = `Unable to setup account. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
}
}
try {
await projectsStore.getUserInvitations();

View File

@ -32,8 +32,10 @@ if (process.env['STORJ_DEBUG_BUNDLE_SIZE']) {
}
export default defineConfig(({ mode }) => {
const isProd = mode === 'production';
// compress chunks only for production mode builds.
if (mode === 'production') {
if (isProd) {
plugins.push(viteCompression({
algorithm: 'brotliCompress',
threshold: 1024,
@ -57,6 +59,7 @@ export default defineConfig(({ mode }) => {
build: {
outDir: resolve(__dirname, 'dist'),
emptyOutDir: true,
reportCompressedSize: isProd,
rollupOptions: {
output: {
experimentalMinChunkSize: 50*1024,