web/satellite: display correct prices in account upgrade modal

The account upgrade modal has been updated to display prices according
to the the user's price model. Previously, the modal displayed only the
default prices which were incorrect for users with price overrides.

Resolves storj/storj-private#187

Change-Id: I58206cc8ea7e7742a37f759a84dbb24ca40dd8eb
This commit is contained in:
Jeremy Wharton 2023-03-16 22:27:27 -05:00
parent 6942da3ddb
commit e0c3f66040
5 changed files with 144 additions and 34 deletions

View File

@ -22,9 +22,9 @@
</div>
<div class="usage-charges-item-container__detailed-info-container__content-area">
<div class="usage-charges-item-container__detailed-info-container__content-area__resource-container">
<p>Storage (${{ storagePrice }} per Gigabyte-Month)</p>
<p>Egress (${{ egressPrice }} per GB)</p>
<p>Segments (${{ segmentPrice }} per Segment-Month)</p>
<p>Storage ({{ storagePrice }} per Gigabyte-Month)</p>
<p>Egress ({{ egressPrice }} per GB)</p>
<p>Segments ({{ segmentPrice }} per Segment-Month)</p>
</div>
<div class="usage-charges-item-container__detailed-info-container__content-area__period-container">
<p>{{ period }}</p>
@ -53,7 +53,7 @@ import { ProjectUsageAndCharges, ProjectUsagePriceModel } from '@/types/payments
import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { decimalShift } from '@/utils/strings';
import { decimalShift, formatPrice, CENTS_MB_TO_DOLLARS_GB_SHIFT } from '@/utils/strings';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useStore } from '@/utils/hooks';
@ -68,12 +68,6 @@ const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
*/
const HOURS_IN_MONTH = 720;
/**
* CENTS_MB_TO_DOLLARS_GB_SHIFT constant represents how many places to the left
* a decimal point must be shifted to convert from cents/MB to dollars/GB.
*/
const CENTS_MB_TO_DOLLARS_GB_SHIFT = -1;
const props = withDefaults(defineProps<{
/**
* item represents usage and charges of current project by period.
@ -144,21 +138,21 @@ const segmentCountFormatted = computed((): string => {
* Returns storage price per GB.
*/
const storagePrice = computed((): string => {
return decimalShift(priceModel.value.storageMBMonthCents, CENTS_MB_TO_DOLLARS_GB_SHIFT);
return formatPrice(decimalShift(priceModel.value.storageMBMonthCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
});
/**
* Returns egress price per GB.
*/
const egressPrice = computed((): string => {
return decimalShift(priceModel.value.egressMBCents, CENTS_MB_TO_DOLLARS_GB_SHIFT);
return formatPrice(decimalShift(priceModel.value.egressMBCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
});
/**
* Returns segment price.
*/
const segmentPrice = computed((): string => {
return decimalShift(priceModel.value.segmentMonthCents, 2);
return formatPrice(decimalShift(priceModel.value.segmentMonthCents, 2));
});
/**

View File

@ -26,9 +26,9 @@
</div>
<div class="usage-charges-item-container__detailed-info-container__content-area">
<div class="usage-charges-item-container__detailed-info-container__content-area__resource-container">
<p>Storage <span class="price-per-month">(${{ storagePrice }} per Gigabyte-Month)</span></p>
<p>Egress <span class="price-per-month">(${{ egressPrice }} per GB)</span></p>
<p>Segments <span class="price-per-month">(${{ segmentPrice }} per Segment-Month)</span></p>
<p>Storage <span class="price-per-month">({{ storagePrice }} per Gigabyte-Month)</span></p>
<p>Egress <span class="price-per-month">({{ egressPrice }} per GB)</span></p>
<p>Segments <span class="price-per-month">({{ segmentPrice }} per Segment-Month)</span></p>
</div>
<div class="usage-charges-item-container__detailed-info-container__content-area__period-container">
<p>{{ period }}</p>
@ -58,7 +58,7 @@ import { ProjectUsageAndCharges, ProjectUsagePriceModel } from '@/types/payments
import { Project } from '@/types/projects';
import { Size } from '@/utils/bytesSize';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { decimalShift } from '@/utils/strings';
import { decimalShift, formatPrice, CENTS_MB_TO_DOLLARS_GB_SHIFT } from '@/utils/strings';
import { useStore } from '@/utils/hooks';
import GreyChevron from '@/../static/images/common/greyChevron.svg';
@ -68,12 +68,6 @@ import GreyChevron from '@/../static/images/common/greyChevron.svg';
*/
const HOURS_IN_MONTH = 720;
/**
* CENTS_MB_TO_DOLLARS_GB_SHIFT constant represents how many places to the left
* a decimal point must be shifted to convert from cents/MB to dollars/GB.
*/
const CENTS_MB_TO_DOLLARS_GB_SHIFT = -1;
const props = withDefaults(defineProps<{
/**
* item represents usage and charges of current project by period.
@ -144,21 +138,21 @@ const segmentCountFormatted = computed((): string => {
* Returns storage price per GB.
*/
const storagePrice = computed((): string => {
return decimalShift(priceModel.value.storageMBMonthCents, CENTS_MB_TO_DOLLARS_GB_SHIFT);
return formatPrice(decimalShift(priceModel.value.storageMBMonthCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
});
/**
* Returns egress price per GB.
*/
const egressPrice = computed((): string => {
return decimalShift(priceModel.value.egressMBCents, CENTS_MB_TO_DOLLARS_GB_SHIFT);
return formatPrice(decimalShift(priceModel.value.egressMBCents, CENTS_MB_TO_DOLLARS_GB_SHIFT));
});
/**
* Returns segment price.
*/
const segmentPrice = computed((): string => {
return decimalShift(priceModel.value.segmentMonthCents, 2);
return formatPrice(decimalShift(priceModel.value.segmentMonthCents, 2));
});
/**

View File

@ -64,15 +64,16 @@
<p class="add-modal__bullets__left__item__label">100 request per second rate limit</p>
</div>
</div>
<div class="add-modal__bullets__right">
<VLoader v-if="isPriceFetching" class="add-modal__bullets__right-loader" width="90px" height="90px" />
<div v-else class="add-modal__bullets__right">
<h2 class="add-modal__bullets__right__title">Storage price:</h2>
<div class="add-modal__bullets__right__item">
<p class="add-modal__bullets__right__item__price">$4</p>
<p class="add-modal__bullets__right__item__price">{{ storagePrice }}</p>
<p class="add-modal__bullets__right__item__label">TB / month</p>
</div>
<h2 class="add-modal__bullets__right__title">Bandwidth price:</h2>
<div class="add-modal__bullets__right__item">
<p class="add-modal__bullets__right__item__price">$7</p>
<p class="add-modal__bullets__right__item__price">{{ bandwidthPrice }}</p>
<p class="add-modal__bullets__right__item__label">TB</p>
</div>
</div>
@ -125,7 +126,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useNotify, useRoute, useStore } from '@/utils/hooks';
import { RouteConfig } from '@/router';
@ -137,6 +138,8 @@ import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { ProjectUsagePriceModel } from '@/types/payments';
import { decimalShift, formatPrice, CENTS_MB_TO_DOLLARS_TB_SHIFT } from '@/utils/strings';
import VModal from '@/components/common/VModal.vue';
import VLoader from '@/components/common/VLoader.vue';
@ -160,9 +163,23 @@ const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isAddModal = ref<boolean>(true);
const isAddCard = ref<boolean>(true);
const isLoading = ref<boolean>(false);
const isPriceFetching = ref<boolean>(true);
const stripeCardInput = ref<StripeCardInput & StripeForm | null>(null);
/**
* Lifecycle hook after initial render.
* Fetches project usage price model.
*/
onMounted(async () => {
try {
await store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_PRICE_MODEL);
isPriceFetching.value = false;
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.UPGRADE_ACCOUNT_MODAL);
}
});
/**
* Provides card information to Stripe.
*/
@ -234,6 +251,29 @@ function setIsAddCard(): void {
const limitsIncreaseRequestURL = computed((): string => {
return MetaUtils.getMetaContent('project-limits-increase-request-url');
});
/**
* Returns project usage price model from store.
*/
const priceModel = computed((): ProjectUsagePriceModel => {
return store.state.paymentsModule.usagePriceModel;
});
/**
* Returns the storage price formatted as dollars per terabyte.
*/
const storagePrice = computed((): string => {
const storage = priceModel.value.storageMBMonthCents.toString();
return formatPrice(decimalShift(storage, CENTS_MB_TO_DOLLARS_TB_SHIFT));
});
/**
* Returns the bandwidth (egress) price formatted as dollars per terabyte.
*/
const bandwidthPrice = computed((): string => {
const egress = priceModel.value.egressMBCents.toString();
return formatPrice(decimalShift(egress, CENTS_MB_TO_DOLLARS_TB_SHIFT));
});
</script>
<style scoped lang="scss">
@ -478,6 +518,16 @@ const limitsIncreaseRequestURL = computed((): string => {
}
}
&__right-loader {
width: 50%;
align-items: center;
@media screen and (max-width: 570px) {
width: 100%;
margin-top: 16px;
}
}
&__right {
padding-left: 50px;

View File

@ -1,6 +1,18 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* CENTS_MB_TO_DOLLARS_GB_SHIFT constant represents how many places to the left
* a decimal point must be shifted to convert from cents/MB to dollars/GB.
*/
export const CENTS_MB_TO_DOLLARS_GB_SHIFT = -1;
/**
* CENTS_MB_TO_DOLLARS_TB_SHIFT constant represents how many places to the left
* a decimal point must be shifted to convert from cents/MB to dollars/TB.
*/
export const CENTS_MB_TO_DOLLARS_TB_SHIFT = -4;
/**
* decimalShift shifts the decimal point of a number represented by the given string.
* @param decimal - the string representation of the number
@ -39,3 +51,30 @@ export function decimalShift(decimal: string, places: number): string {
}
return sign + (int || '0') + (frac ? '.' + frac : '');
}
/**
* formatPrice formats the decimal string as a price.
* @param decimal - the decimal string to format
*/
export function formatPrice(decimal: string) {
let sign = '';
if (decimal[0] === '-') {
sign = '-';
decimal = decimal.substring(1);
}
const parts = decimal.split('.');
const int = parts[0]?.replace(/^0+/, '');
let frac = '';
if (parts.length > 1) {
frac = parts[1].replace(/0+$/, '');
if (frac) {
frac = frac.padEnd(2, '0');
}
}
if (!int && !frac) {
return '$0';
}
return sign + '$' + (int || '0') + (frac ? '.' + frac : '');
}

View File

@ -1,10 +1,16 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { decimalShift } from '@/utils/strings';
import { decimalShift, formatPrice } from '@/utils/strings';
describe('decimalShift', (): void => {
it('shifts integers correctly', function() {
it('handles empty strings', (): void => {
expect(decimalShift('', 0)).toBe('0');
expect(decimalShift('', 2)).toBe('0');
expect(decimalShift('', -2)).toBe('0');
});
it('shifts integers correctly', (): void => {
['', '-'].forEach(sign => {
const decimal = sign+'123';
expect(decimalShift(decimal, 0)).toBe(sign+'123');
@ -14,7 +20,7 @@ describe('decimalShift', (): void => {
});
});
it('shifts decimals correctly', function() {
it('shifts decimals correctly', (): void => {
['', '-'].forEach(sign => {
const decimal = sign+'1.23';
expect(decimalShift(decimal, 0)).toBe(sign+'1.23');
@ -24,7 +30,7 @@ describe('decimalShift', (): void => {
});
});
it('trims unnecessary characters', function() {
it('trims unnecessary characters', (): void => {
['', '-'].forEach(sign => {
expect(decimalShift(sign+'0.0012300', -2)).toBe(sign+'0.123');
expect(decimalShift(sign+'12300', 2)).toBe(sign+'123');
@ -32,3 +38,30 @@ describe('decimalShift', (): void => {
});
});
});
describe('formatPrice', (): void => {
it('handles empty strings', (): void => {
expect(formatPrice('')).toBe('$0');
});
it('formats correctly', (): void => {
['', '-'].forEach(sign => {
expect(formatPrice(sign+'123')).toBe(sign+'$123');
expect(formatPrice(sign+'1.002')).toBe(sign+'$1.002');
});
});
it('adds zeros when necessary', (): void => {
['', '-'].forEach(sign => {
expect(formatPrice(sign+'12.3')).toBe(sign+'$12.30');
expect(formatPrice(sign+'.123')).toBe(sign+'$0.123');
});
});
it('trims unnecessary characters', (): void => {
['', '-'].forEach(sign => {
expect(formatPrice(sign+'0.0')).toBe('$0');
expect(formatPrice(sign+'00123.00')).toBe(sign+'$123');
});
});
});