@stigg/react-sdk 5.2.1 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/common/TiersSelectContainer.d.ts +2 -4
- package/dist/components/common/VolumeBulkSelect.d.ts +1 -1
- package/dist/components/common/VolumePerUnitInput.d.ts +1 -1
- package/dist/components/paywall/PlanOfferingButton.d.ts +2 -3
- package/dist/components/paywall/PlanPrice.d.ts +2 -4
- package/dist/components/utils/fixtures/price.fixtures.d.ts +7 -0
- package/dist/components/utils/getPaidPriceText.d.ts +2 -3
- package/dist/components/utils/getPlanPrice.d.ts +1 -1
- package/dist/components/utils/priceTierUtils.d.ts +11 -6
- package/dist/components/utils/priceTierUtils.spec.d.ts +1 -0
- package/dist/react-sdk.cjs.development.js +137 -188
- package/dist/react-sdk.cjs.development.js.map +1 -1
- package/dist/react-sdk.cjs.production.min.js +1 -1
- package/dist/react-sdk.cjs.production.min.js.map +1 -1
- package/dist/react-sdk.esm.js +140 -191
- package/dist/react-sdk.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/components/checkout/hooks/usePlanStepModel.ts +1 -1
- package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +4 -11
- package/src/components/checkout/summary/components/LineItems.tsx +5 -8
- package/src/components/common/TiersSelectContainer.tsx +5 -16
- package/src/components/common/VolumeBulkSelect.tsx +7 -4
- package/src/components/common/VolumePerUnitInput.tsx +3 -5
- package/src/components/paywall/Paywall.tsx +2 -2
- package/src/components/paywall/PlanOffering.tsx +6 -27
- package/src/components/paywall/PlanOfferingButton.tsx +2 -8
- package/src/components/paywall/PlanPrice.tsx +4 -23
- package/src/components/utils/calculateDiscountRate.ts +2 -2
- package/src/components/utils/fixtures/price.fixtures.ts +42 -0
- package/src/components/utils/getPaidPriceText.ts +6 -24
- package/src/components/utils/getPlanPrice.ts +0 -2
- package/src/components/utils/priceTierUtils.spec.ts +84 -0
- package/src/components/utils/priceTierUtils.ts +78 -73
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.4.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "tsdx watch",
|
|
15
15
|
"build": "tsdx build",
|
|
16
|
-
"test": "tsdx test
|
|
16
|
+
"test": "tsdx test",
|
|
17
17
|
"lint": "eslint src --ext .ts,.tsx --max-warnings=0",
|
|
18
18
|
"lint-fix": "yarn run lint --fix",
|
|
19
19
|
"size": "size-limit",
|
|
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
3
3
|
import styled from '@emotion/styled';
|
|
4
4
|
import Box from '@mui/material/Box';
|
|
5
5
|
import { BillableFeatureInput } from '@stigg/api-client-js/src/generated/sdk';
|
|
6
|
-
import { BillingModel, BillingPeriod, Plan, Price
|
|
6
|
+
import { BillingModel, BillingPeriod, Plan, Price } from '@stigg/js-client-sdk';
|
|
7
7
|
|
|
8
8
|
import { Typography } from '../../../common/Typography';
|
|
9
9
|
import { useChargesSort } from '../../../hooks/useChargeSort';
|
|
@@ -12,7 +12,6 @@ import { currencyPriceFormatter } from '../../../utils/currencyUtils';
|
|
|
12
12
|
import { InputField } from '../../components';
|
|
13
13
|
import { useCheckoutModel, usePlanStepModel, useProgressBarModel } from '../../hooks';
|
|
14
14
|
import { TiersSelectContainer } from '../../../common/TiersSelectContainer';
|
|
15
|
-
import { getTierByQuantity } from '../../../utils/priceTierUtils';
|
|
16
15
|
import { getValidPriceQuantity } from '../../../utils/priceUtils';
|
|
17
16
|
import { getFeatureDisplayNameText } from '../../../utils/getFeatureName';
|
|
18
17
|
|
|
@@ -104,20 +103,14 @@ export function PlanCharge({
|
|
|
104
103
|
chargeRow += ' / unit';
|
|
105
104
|
}
|
|
106
105
|
} else if (charge.isTieredPrice && charge.tiersMode && featureId) {
|
|
107
|
-
const
|
|
106
|
+
const quantity = billableFeature?.quantity || 1;
|
|
108
107
|
chargeRow = (
|
|
109
108
|
<TiersSelectContainer
|
|
110
109
|
componentId={`${featureId}-tiers`}
|
|
111
110
|
tiers={charge.tiers}
|
|
112
|
-
selectedTier={tier}
|
|
113
|
-
handleTierChange={(tier: PriceTierFragment) => {
|
|
114
|
-
if (tier.upTo) {
|
|
115
|
-
setBillableFeature(featureId, tier.upTo);
|
|
116
|
-
}
|
|
117
|
-
}}
|
|
118
111
|
tiersMode={charge.tiersMode}
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
value={quantity}
|
|
113
|
+
handleChange={(quantity: number) => {
|
|
121
114
|
setBillableFeature(featureId, quantity);
|
|
122
115
|
}}
|
|
123
116
|
width={120}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '@stigg/js-client-sdk';
|
|
11
11
|
import { Typography } from '../../../common/Typography';
|
|
12
12
|
import { currencyPriceFormatter } from '../../../utils/currencyUtils';
|
|
13
|
-
import {
|
|
13
|
+
import { calculateTierPrice } from '../../../utils/priceTierUtils';
|
|
14
14
|
import { WithSkeleton } from './WithSkeleton';
|
|
15
15
|
import { Skeleton } from '../../components/Skeletons.style';
|
|
16
16
|
import { CheckoutLocalization } from '../../configurations/textOverrides';
|
|
@@ -74,14 +74,11 @@ export const BilledPriceLineItem = ({
|
|
|
74
74
|
|
|
75
75
|
let amount;
|
|
76
76
|
if (price.isTieredPrice) {
|
|
77
|
-
|
|
78
|
-
amount = calculateTierPriceVolume(tier!, quantity);
|
|
77
|
+
amount = calculateTierPrice(price, quantity);
|
|
79
78
|
} else {
|
|
80
|
-
amount = price.amount
|
|
79
|
+
amount = price.amount! * quantity;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
const totalLineAmount = price.isTieredPrice ? amount : amount * quantity;
|
|
84
|
-
|
|
85
82
|
return (
|
|
86
83
|
<LineItemContainer>
|
|
87
84
|
<LineItemRow style={{ alignItems: 'flex-start' }}>
|
|
@@ -91,7 +88,7 @@ export const BilledPriceLineItem = ({
|
|
|
91
88
|
</Typography>
|
|
92
89
|
{(quantity > 1 || billingPeriod === BillingPeriod.Annually) && (
|
|
93
90
|
<Typography variant="body1" color="secondary">
|
|
94
|
-
{getPriceString({ amount
|
|
91
|
+
{getPriceString({ amount, price, quantity })}
|
|
95
92
|
</Typography>
|
|
96
93
|
)}
|
|
97
94
|
</Grid>
|
|
@@ -99,7 +96,7 @@ export const BilledPriceLineItem = ({
|
|
|
99
96
|
{isPayAsYouGo && <PayAsYouGoPriceTooltip checkoutLocalization={checkoutLocalization} />}
|
|
100
97
|
<Typography variant="body1" color="secondary" style={{ wordBreak: 'break-word' }}>
|
|
101
98
|
{currencyPriceFormatter({
|
|
102
|
-
amount
|
|
99
|
+
amount,
|
|
103
100
|
currency: price.currency,
|
|
104
101
|
minimumFractionDigits: 2,
|
|
105
102
|
})}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PriceTierFragment, TiersMode } from '@stigg/js-client-sdk';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import styled from '@emotion/styled/macro';
|
|
4
|
-
import {
|
|
4
|
+
import { hasTierWithUnitPrice } from '../utils/priceTierUtils';
|
|
5
5
|
import { Typography } from './Typography';
|
|
6
6
|
import { VolumePerUnitInput } from './VolumePerUnitInput';
|
|
7
7
|
import { VolumeBulkSelect } from './VolumeBulkSelect';
|
|
@@ -10,11 +10,9 @@ export type TiersSelectContainerProps = {
|
|
|
10
10
|
componentId: string;
|
|
11
11
|
tiers?: PriceTierFragment[] | null;
|
|
12
12
|
tierUnits?: string;
|
|
13
|
-
selectedTier?: PriceTierFragment;
|
|
14
|
-
handleTierChange: (tier: PriceTierFragment) => void;
|
|
15
13
|
tiersMode?: TiersMode | null;
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
handleChange: (quantity: number) => void;
|
|
15
|
+
value: number;
|
|
18
16
|
width?: number;
|
|
19
17
|
};
|
|
20
18
|
|
|
@@ -23,20 +21,11 @@ const TiersSelectLayout = styled(Typography)`
|
|
|
23
21
|
`;
|
|
24
22
|
|
|
25
23
|
export function TiersSelectContainer(props: TiersSelectContainerProps) {
|
|
26
|
-
const { tiers
|
|
27
|
-
const handleChange = (quantity: number) => {
|
|
28
|
-
if (!tiers) return;
|
|
29
|
-
|
|
30
|
-
handleTierChange(getTierByQuantity(tiers, quantity)!);
|
|
31
|
-
};
|
|
24
|
+
const { tiers } = props;
|
|
32
25
|
|
|
33
26
|
return (
|
|
34
27
|
<TiersSelectLayout as="div" className="stigg-price-tier-select">
|
|
35
|
-
{hasTierWithUnitPrice(tiers) ?
|
|
36
|
-
<VolumePerUnitInput handleChange={handleChange} {...props} />
|
|
37
|
-
) : (
|
|
38
|
-
<VolumeBulkSelect handleChange={handleChange} {...props} />
|
|
39
|
-
)}
|
|
28
|
+
{hasTierWithUnitPrice(tiers) ? <VolumePerUnitInput {...props} /> : <VolumeBulkSelect {...props} />}
|
|
40
29
|
</TiersSelectLayout>
|
|
41
30
|
);
|
|
42
31
|
}
|
|
@@ -29,12 +29,12 @@ const TierInput = styled(OutlinedInput)`
|
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
export function VolumeBulkSelect({
|
|
32
|
-
|
|
32
|
+
value,
|
|
33
33
|
componentId,
|
|
34
34
|
tierUnits,
|
|
35
35
|
tiers,
|
|
36
36
|
handleChange,
|
|
37
|
-
}: Pick<TiersSelectContainerProps, '
|
|
37
|
+
}: Pick<TiersSelectContainerProps, 'value' | 'componentId' | 'tiers' | 'tierUnits'> & {
|
|
38
38
|
handleChange: (quantity: number) => void;
|
|
39
39
|
}) {
|
|
40
40
|
if (!tiers) {
|
|
@@ -43,10 +43,13 @@ export function VolumeBulkSelect({
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<TierSelect
|
|
46
|
-
value={
|
|
46
|
+
value={value.toString()}
|
|
47
47
|
fullWidth
|
|
48
48
|
onChange={(event) => {
|
|
49
|
-
|
|
49
|
+
const quantity = event?.target?.value ? Number(event?.target?.value) : null;
|
|
50
|
+
if (quantity) {
|
|
51
|
+
handleChange(quantity);
|
|
52
|
+
}
|
|
50
53
|
}}
|
|
51
54
|
id={componentId}
|
|
52
55
|
input={<TierInput />}
|
|
@@ -7,10 +7,9 @@ import { TiersSelectContainerProps } from './TiersSelectContainer';
|
|
|
7
7
|
export function VolumePerUnitInput({
|
|
8
8
|
width,
|
|
9
9
|
tierUnits,
|
|
10
|
-
|
|
11
|
-
handlePerUnitQuantityChange,
|
|
10
|
+
value,
|
|
12
11
|
handleChange,
|
|
13
|
-
}: Pick<TiersSelectContainerProps, 'width' | 'tierUnits' | '
|
|
12
|
+
}: Pick<TiersSelectContainerProps, 'width' | 'tierUnits' | 'value'> & {
|
|
14
13
|
handleChange: (quantity: number) => void;
|
|
15
14
|
}) {
|
|
16
15
|
return (
|
|
@@ -27,12 +26,11 @@ export function VolumePerUnitInput({
|
|
|
27
26
|
</InputAdornment>
|
|
28
27
|
),
|
|
29
28
|
}}
|
|
30
|
-
value={
|
|
29
|
+
value={value}
|
|
31
30
|
onChange={(event) => {
|
|
32
31
|
const quantity = event?.target?.value ? Number(event?.target?.value) : null;
|
|
33
32
|
if (quantity) {
|
|
34
33
|
handleChange(quantity);
|
|
35
|
-
handlePerUnitQuantityChange(quantity);
|
|
36
34
|
}
|
|
37
35
|
}}
|
|
38
36
|
/>
|
|
@@ -17,7 +17,7 @@ import { useStiggContext } from '../..';
|
|
|
17
17
|
import { hasPricePointsForPlans } from './utils/hasPricePoints';
|
|
18
18
|
import { getPlansToDisplay } from './utils/getPlansToDisplay';
|
|
19
19
|
import { getPlanPrice } from '../utils/getPlanPrice';
|
|
20
|
-
import {
|
|
20
|
+
import { getTiersPerUnitQuantities } from '../utils/priceTierUtils';
|
|
21
21
|
|
|
22
22
|
const PaywallPlansContainer = styled.div`
|
|
23
23
|
color: ${({ theme }) => theme.stigg.palette.text.primary};
|
|
@@ -123,7 +123,7 @@ export const Paywall = ({
|
|
|
123
123
|
return (
|
|
124
124
|
!isCustomerInCustomPlan &&
|
|
125
125
|
plansToShow.some((plan) => {
|
|
126
|
-
const tiers =
|
|
126
|
+
const tiers = getTiersPerUnitQuantities(plan, selectedBillingPeriod, currentSubscription);
|
|
127
127
|
return Object.values(tiers).length > 0;
|
|
128
128
|
})
|
|
129
129
|
);
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
BillableFeature,
|
|
4
|
-
BillingPeriod,
|
|
5
|
-
Customer,
|
|
6
|
-
PriceTierFragment,
|
|
7
|
-
PricingType,
|
|
8
|
-
Subscription,
|
|
9
|
-
} from '@stigg/js-client-sdk';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { BillableFeature, BillingPeriod, Customer, PricingType, Subscription } from '@stigg/js-client-sdk';
|
|
10
3
|
import styled from '@emotion/styled/macro';
|
|
11
4
|
import classNames from 'classnames';
|
|
12
5
|
import Grid from '@mui/material/Grid';
|
|
@@ -18,7 +11,7 @@ import { flexLayoutMapper } from '../../theme/getResolvedTheme';
|
|
|
18
11
|
import { Typography } from '../common/Typography';
|
|
19
12
|
import MiniSchedule from '../../assets/mini-schedule.svg';
|
|
20
13
|
import { PlanPrice } from './PlanPrice';
|
|
21
|
-
import {
|
|
14
|
+
import { getTiersPerUnitQuantities } from '../utils/priceTierUtils';
|
|
22
15
|
|
|
23
16
|
const PlanOfferingButtonHeight = '66px';
|
|
24
17
|
|
|
@@ -167,28 +160,17 @@ export function PlanOffering({
|
|
|
167
160
|
);
|
|
168
161
|
}
|
|
169
162
|
|
|
170
|
-
const [perUnitQuantityByFeature, setPerUnitQuantityByFeature] = useState<Record<string, number>>(
|
|
171
|
-
|
|
172
|
-
getSelectedTier(plan, billingPeriod, currentSubscription, {}, selectDefaultTierIndex),
|
|
163
|
+
const [perUnitQuantityByFeature, setPerUnitQuantityByFeature] = useState<Record<string, number>>(
|
|
164
|
+
getTiersPerUnitQuantities(plan, billingPeriod, currentSubscription, selectDefaultTierIndex),
|
|
173
165
|
);
|
|
174
166
|
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
setSelectedTierByFeature(getSelectedTier(plan, billingPeriod, currentSubscription, selectedTierByFeature));
|
|
177
|
-
setPerUnitQuantityByFeature(getSelectedTierQuantityBeFeature(plan, billingPeriod, currentSubscription));
|
|
178
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
179
|
-
}, [billingPeriod, currentSubscription, plan]);
|
|
180
|
-
|
|
181
167
|
const onPlanButtonClick = (intentionType: SubscribeIntentionType) => {
|
|
182
|
-
const tierBillableFeatures: BillableFeature[] = Object.keys(selectedTierByFeature).map((featureId) => ({
|
|
183
|
-
featureId,
|
|
184
|
-
quantity: selectedTierByFeature[featureId].upTo || 0,
|
|
185
|
-
}));
|
|
186
168
|
const perUnitBillableFeatures: BillableFeature[] = Object.keys(perUnitQuantityByFeature).map((featureId) => ({
|
|
187
169
|
featureId,
|
|
188
170
|
quantity: perUnitQuantityByFeature[featureId],
|
|
189
171
|
}));
|
|
190
172
|
|
|
191
|
-
return onPlanSelected(intentionType,
|
|
173
|
+
return onPlanSelected(intentionType, perUnitBillableFeatures);
|
|
192
174
|
};
|
|
193
175
|
|
|
194
176
|
return (
|
|
@@ -216,8 +198,6 @@ export function PlanOffering({
|
|
|
216
198
|
withUnitPriceRow={withUnitPriceRow}
|
|
217
199
|
withStartingAtRow={withStartingAtRow}
|
|
218
200
|
withTiersRow={withTiersRow}
|
|
219
|
-
selectedTierByFeature={selectedTierByFeature}
|
|
220
|
-
setSelectedTierByFeature={setSelectedTierByFeature}
|
|
221
201
|
plan={plan}
|
|
222
202
|
billingPeriod={billingPeriod}
|
|
223
203
|
paywallLocale={paywallLocale}
|
|
@@ -239,7 +219,6 @@ export function PlanOffering({
|
|
|
239
219
|
onPlanSelected={onPlanButtonClick}
|
|
240
220
|
paywallLocale={paywallLocale}
|
|
241
221
|
withTrialLeftRow={withTrialLeftRow}
|
|
242
|
-
selectedTierByFeature={selectedTierByFeature}
|
|
243
222
|
perUnitQuantityByFeature={perUnitQuantityByFeature}
|
|
244
223
|
/>
|
|
245
224
|
) : (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { BillingPeriod, Customer,
|
|
2
|
+
import { BillingPeriod, Customer, PricingType, Subscription } from '@stigg/js-client-sdk';
|
|
3
3
|
import { isFunction } from 'lodash';
|
|
4
4
|
import ClipLoader from 'react-spinners/ClipLoader';
|
|
5
5
|
import styled from '@emotion/styled/macro';
|
|
@@ -82,7 +82,6 @@ type PlanOfferingButtonProps = {
|
|
|
82
82
|
paywallLocale: PaywallLocalization;
|
|
83
83
|
onPlanSelected: (intentionType: SubscribeIntentionType) => void | Promise<void>;
|
|
84
84
|
withTrialLeftRow: boolean;
|
|
85
|
-
selectedTierByFeature: Record<string, PriceTierFragment>;
|
|
86
85
|
perUnitQuantityByFeature: Record<string, number>;
|
|
87
86
|
};
|
|
88
87
|
|
|
@@ -96,7 +95,6 @@ export function PlanOfferingButton({
|
|
|
96
95
|
paywallLocale,
|
|
97
96
|
withTrialLeftRow,
|
|
98
97
|
currentSubscription,
|
|
99
|
-
selectedTierByFeature,
|
|
100
98
|
perUnitQuantityByFeature,
|
|
101
99
|
}: PlanOfferingButtonProps) {
|
|
102
100
|
const theme = useTheme();
|
|
@@ -144,11 +142,7 @@ export function PlanOfferingButton({
|
|
|
144
142
|
isSameBillingPeriod ||
|
|
145
143
|
(plan.pricingType && [PricingType.Free, PricingType.Custom].includes(plan.pricingType))
|
|
146
144
|
) {
|
|
147
|
-
const planComparison = compareSelectedTierToCurrentTier(
|
|
148
|
-
selectedTierByFeature,
|
|
149
|
-
perUnitQuantityByFeature,
|
|
150
|
-
currentSubscription,
|
|
151
|
-
);
|
|
145
|
+
const planComparison = compareSelectedTierToCurrentTier(perUnitQuantityByFeature, currentSubscription);
|
|
152
146
|
switch (planComparison) {
|
|
153
147
|
case PriceTierComparison.Lower:
|
|
154
148
|
buttonProps.intentionType = SubscribeIntentionType.CHANGE_UNIT_QUANTITY;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import styled from '@emotion/styled/macro';
|
|
3
|
-
import { BillingPeriod
|
|
3
|
+
import { BillingPeriod } from '@stigg/js-client-sdk';
|
|
4
4
|
import { PaywallPlan } from './types';
|
|
5
5
|
import { PaywallLocalization } from './paywallTextOverrides';
|
|
6
6
|
import { getPlanPrice } from '../utils/getPlanPrice';
|
|
@@ -57,8 +57,6 @@ export const PlanPrice = ({
|
|
|
57
57
|
withUnitPriceRow,
|
|
58
58
|
withStartingAtRow,
|
|
59
59
|
withTiersRow,
|
|
60
|
-
selectedTierByFeature,
|
|
61
|
-
setSelectedTierByFeature,
|
|
62
60
|
plan,
|
|
63
61
|
billingPeriod,
|
|
64
62
|
paywallLocale,
|
|
@@ -72,8 +70,6 @@ export const PlanPrice = ({
|
|
|
72
70
|
withUnitPriceRow: boolean;
|
|
73
71
|
withStartingAtRow: boolean;
|
|
74
72
|
withTiersRow: boolean;
|
|
75
|
-
selectedTierByFeature: Record<string, PriceTierFragment>;
|
|
76
|
-
setSelectedTierByFeature: React.Dispatch<React.SetStateAction<Record<string, PriceTierFragment>>>;
|
|
77
73
|
perUnitQuantityByFeature: Record<string, number>;
|
|
78
74
|
setPerUnitQuantityByFeature: React.Dispatch<React.SetStateAction<Record<string, number>>>;
|
|
79
75
|
plan: PaywallPlan;
|
|
@@ -89,7 +85,6 @@ export const PlanPrice = ({
|
|
|
89
85
|
paywallLocale,
|
|
90
86
|
locale,
|
|
91
87
|
hasMonthlyPrice,
|
|
92
|
-
selectedTierByFeature,
|
|
93
88
|
perUnitQuantityByFeature,
|
|
94
89
|
);
|
|
95
90
|
|
|
@@ -98,19 +93,7 @@ export const PlanPrice = ({
|
|
|
98
93
|
return planPrice.billingPeriod === billingPeriod && planPrice.isTieredPrice;
|
|
99
94
|
});
|
|
100
95
|
const featureId = tieredPrice ? tieredPrice.feature!.featureId : undefined;
|
|
101
|
-
const
|
|
102
|
-
const perUnitQuantity = featureId ? perUnitQuantityByFeature[featureId] : undefined;
|
|
103
|
-
|
|
104
|
-
const handleTierChange = (tier: PriceTierFragment) => {
|
|
105
|
-
if (!featureId) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
setSelectedTierByFeature((prevState) => ({
|
|
110
|
-
...prevState,
|
|
111
|
-
[featureId]: tier,
|
|
112
|
-
}));
|
|
113
|
-
};
|
|
96
|
+
const perUnitQuantity = perUnitQuantityByFeature[featureId || ''] ?? 1;
|
|
114
97
|
|
|
115
98
|
const handlePerUnitQuantityChange = (quantity: number) => {
|
|
116
99
|
if (!featureId) {
|
|
@@ -161,11 +144,9 @@ export const PlanPrice = ({
|
|
|
161
144
|
componentId={`${plan.id}_${featureId}_tier`}
|
|
162
145
|
tiers={tiers}
|
|
163
146
|
tierUnits={tierUnits}
|
|
164
|
-
selectedTier={selectedTier}
|
|
165
|
-
handleTierChange={handleTierChange}
|
|
166
147
|
tiersMode={tieredPrice?.tiersMode}
|
|
167
|
-
|
|
168
|
-
|
|
148
|
+
handleChange={handlePerUnitQuantityChange}
|
|
149
|
+
value={perUnitQuantity}
|
|
169
150
|
/>
|
|
170
151
|
) : null}
|
|
171
152
|
</>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BillingPeriod, PaywallCalculatedPricePoint, Price } from '@stigg/js-client-sdk';
|
|
2
2
|
import isNil from 'lodash/isNil';
|
|
3
3
|
import { PaywallPlan } from '../paywall';
|
|
4
|
-
import {
|
|
4
|
+
import { calculateTierPrice } from './priceTierUtils';
|
|
5
5
|
|
|
6
6
|
export function calculateDiscountRate(monthlyPrice?: number | null, annuallyPrice?: number | null) {
|
|
7
7
|
if (!isNil(monthlyPrice) && !isNil(annuallyPrice)) {
|
|
@@ -27,7 +27,7 @@ function getPlanBillingPeriodAmount(plan: PaywallPlan, billingPeriod: BillingPer
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
if (tieredPrice) {
|
|
30
|
-
return
|
|
30
|
+
return calculateTierPrice(tieredPrice, 1);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Currency, Price, TiersMode } from '@stigg/js-client-sdk';
|
|
2
|
+
|
|
3
|
+
export const money = (amount: number, currency: Currency) => ({ amount, currency });
|
|
4
|
+
|
|
5
|
+
export const priceTiersFlat = (tiersMode: TiersMode, currency: Currency): Pick<Price, 'tiers' | 'tiersMode'> => {
|
|
6
|
+
return {
|
|
7
|
+
tiersMode,
|
|
8
|
+
tiers: [
|
|
9
|
+
{
|
|
10
|
+
upTo: 10,
|
|
11
|
+
flatPrice: money(100, currency),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
upTo: 20,
|
|
15
|
+
flatPrice: money(180, currency),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
upTo: 30,
|
|
19
|
+
flatPrice: money(240, currency),
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const priceTiersUnit = (tiersMode: TiersMode, currency: Currency): Pick<Price, 'tiers' | 'tiersMode'> => {
|
|
26
|
+
return {
|
|
27
|
+
tiersMode,
|
|
28
|
+
tiers: [
|
|
29
|
+
{
|
|
30
|
+
upTo: 10,
|
|
31
|
+
unitPrice: money(10, currency),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
upTo: 20,
|
|
35
|
+
unitPrice: money(9, currency),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
unitPrice: money(8, currency),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BillingModel,
|
|
3
|
-
BillingPeriod,
|
|
4
|
-
PaywallCalculatedPricePoint,
|
|
5
|
-
Price,
|
|
6
|
-
PriceTierFragment,
|
|
7
|
-
} from '@stigg/js-client-sdk';
|
|
1
|
+
import { BillingModel, BillingPeriod, PaywallCalculatedPricePoint, Price } from '@stigg/js-client-sdk';
|
|
8
2
|
import { currencyPriceFormatter } from './currencyUtils';
|
|
9
3
|
import { PlanPriceText } from './getPlanPrice';
|
|
10
4
|
import { calculateTierPrice, getPriceFeatureUnit } from './priceTierUtils';
|
|
@@ -16,7 +10,6 @@ type GetPaidPriceTextParams = {
|
|
|
16
10
|
selectedBillingPeriod: BillingPeriod;
|
|
17
11
|
locale: string;
|
|
18
12
|
shouldShowMonthlyPriceAmount: boolean;
|
|
19
|
-
selectedTierByFeature: Record<string, PriceTierFragment>;
|
|
20
13
|
paywallLocale: PaywallLocalization;
|
|
21
14
|
perUnitQuantityByFeature?: Record<string, number>;
|
|
22
15
|
};
|
|
@@ -27,7 +20,6 @@ export function getPaidPriceText({
|
|
|
27
20
|
selectedBillingPeriod,
|
|
28
21
|
locale,
|
|
29
22
|
shouldShowMonthlyPriceAmount,
|
|
30
|
-
selectedTierByFeature,
|
|
31
23
|
paywallLocale,
|
|
32
24
|
perUnitQuantityByFeature,
|
|
33
25
|
}: GetPaidPriceTextParams): PlanPriceText {
|
|
@@ -46,22 +38,12 @@ export function getPaidPriceText({
|
|
|
46
38
|
let unit = pricePeriod;
|
|
47
39
|
|
|
48
40
|
for (const price of planPrices) {
|
|
49
|
-
if (price.isTieredPrice) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
if (price.isTieredPrice && price.tiers) {
|
|
42
|
+
const quantity = perUnitQuantityByFeature?.[price.feature!.featureId] || 1;
|
|
43
|
+
tiers = price.tiers;
|
|
44
|
+
tierUnits = getPriceFeatureUnit(price);
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
tierUnits = getPriceFeatureUnit(price);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
priceNumber += calculateTierPrice(
|
|
59
|
-
price,
|
|
60
|
-
currentTier,
|
|
61
|
-
selectedBillingPeriod,
|
|
62
|
-
shouldShowMonthlyPriceAmount,
|
|
63
|
-
perUnitQuantityByFeature?.[price.feature!.featureId] || 1,
|
|
64
|
-
);
|
|
46
|
+
priceNumber += calculateTierPrice(price, quantity, selectedBillingPeriod, shouldShowMonthlyPriceAmount);
|
|
65
47
|
}
|
|
66
48
|
}
|
|
67
49
|
|
|
@@ -15,7 +15,6 @@ export function getPlanPrice(
|
|
|
15
15
|
paywallLocale: PaywallLocalization,
|
|
16
16
|
locale: string,
|
|
17
17
|
shouldShowMonthlyPriceAmount: boolean,
|
|
18
|
-
selectedTierByFeature?: Record<string, PriceTierFragment>,
|
|
19
18
|
perUnitQuantityByFeature?: Record<string, number>,
|
|
20
19
|
): PlanPriceText {
|
|
21
20
|
switch (plan.pricingType) {
|
|
@@ -48,7 +47,6 @@ export function getPlanPrice(
|
|
|
48
47
|
planPrices,
|
|
49
48
|
paywallCalculatedPrice,
|
|
50
49
|
selectedBillingPeriod: billingPeriod,
|
|
51
|
-
selectedTierByFeature: selectedTierByFeature || {},
|
|
52
50
|
perUnitQuantityByFeature: perUnitQuantityByFeature || {},
|
|
53
51
|
};
|
|
54
52
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Currency, TiersMode } from '@stigg/js-client-sdk';
|
|
2
|
+
import { priceTiersFlat, priceTiersUnit } from './fixtures/price.fixtures';
|
|
3
|
+
import { calculateTierPrice, calculateTierPriceGraduated } from './priceTierUtils';
|
|
4
|
+
|
|
5
|
+
describe('getPrice tests', () => {
|
|
6
|
+
describe('volume flat price', () => {
|
|
7
|
+
test('should get total price', () => {
|
|
8
|
+
const price = priceTiersFlat(TiersMode.Volume, Currency.Usd);
|
|
9
|
+
expect(calculateTierPrice(price, 1)).toEqual(100);
|
|
10
|
+
expect(calculateTierPrice(price, 3)).toEqual(100);
|
|
11
|
+
expect(calculateTierPrice(price, 7)).toEqual(100);
|
|
12
|
+
expect(calculateTierPrice(price, 10)).toEqual(100);
|
|
13
|
+
expect(calculateTierPrice(price, 11)).toEqual(180);
|
|
14
|
+
expect(calculateTierPrice(price, 14)).toEqual(180);
|
|
15
|
+
expect(calculateTierPrice(price, 20)).toEqual(180);
|
|
16
|
+
expect(calculateTierPrice(price, 21)).toEqual(240);
|
|
17
|
+
expect(calculateTierPrice(price, 25)).toEqual(240);
|
|
18
|
+
expect(calculateTierPrice(price, 30)).toEqual(240);
|
|
19
|
+
|
|
20
|
+
// questionable behaviour, but that's the current implementation :)
|
|
21
|
+
expect(calculateTierPrice(price, 35)).toEqual(240);
|
|
22
|
+
expect(calculateTierPrice(price, 250)).toEqual(240);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('volume per unit', () => {
|
|
27
|
+
test('should get total price', () => {
|
|
28
|
+
const price = priceTiersUnit(TiersMode.Volume, Currency.Usd);
|
|
29
|
+
expect(calculateTierPrice(price, 1)).toEqual(10);
|
|
30
|
+
expect(calculateTierPrice(price, 3)).toEqual(30);
|
|
31
|
+
expect(calculateTierPrice(price, 7)).toEqual(70);
|
|
32
|
+
expect(calculateTierPrice(price, 10)).toEqual(100);
|
|
33
|
+
expect(calculateTierPrice(price, 11)).toEqual(99);
|
|
34
|
+
expect(calculateTierPrice(price, 14)).toEqual(126);
|
|
35
|
+
expect(calculateTierPrice(price, 20)).toEqual(180);
|
|
36
|
+
expect(calculateTierPrice(price, 21)).toEqual(168);
|
|
37
|
+
expect(calculateTierPrice(price, 25)).toEqual(200);
|
|
38
|
+
expect(calculateTierPrice(price, 30)).toEqual(240);
|
|
39
|
+
expect(calculateTierPrice(price, 35)).toEqual(280);
|
|
40
|
+
expect(calculateTierPrice(price, 240)).toEqual(1_920);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('graduated', () => {
|
|
45
|
+
test('should get total price', () => {
|
|
46
|
+
const price = priceTiersUnit(TiersMode.Graduated, Currency.Usd);
|
|
47
|
+
expect(calculateTierPrice(price, 1)).toEqual(10);
|
|
48
|
+
expect(calculateTierPrice(price, 3)).toEqual(30);
|
|
49
|
+
expect(calculateTierPrice(price, 7)).toEqual(70);
|
|
50
|
+
expect(calculateTierPrice(price, 10)).toEqual(100);
|
|
51
|
+
expect(calculateTierPrice(price, 11)).toEqual(109);
|
|
52
|
+
expect(calculateTierPrice(price, 14)).toEqual(136);
|
|
53
|
+
expect(calculateTierPrice(price, 20)).toEqual(190);
|
|
54
|
+
expect(calculateTierPrice(price, 21)).toEqual(198);
|
|
55
|
+
expect(calculateTierPrice(price, 25)).toEqual(230);
|
|
56
|
+
expect(calculateTierPrice(price, 30)).toEqual(270);
|
|
57
|
+
expect(calculateTierPrice(price, 35)).toEqual(310);
|
|
58
|
+
expect(calculateTierPrice(price, 240)).toEqual(1_950);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should return nested breakdown', () => {
|
|
62
|
+
const price = priceTiersUnit(TiersMode.Graduated, Currency.Usd);
|
|
63
|
+
|
|
64
|
+
const result1 = calculateTierPriceGraduated(price.tiers!, 10);
|
|
65
|
+
expect(result1.total).toEqual(100);
|
|
66
|
+
expect(result1.breakdown).toEqual([{ unitQuantity: 10, amount: 100 }]);
|
|
67
|
+
|
|
68
|
+
const result2 = calculateTierPriceGraduated(price.tiers!, 13);
|
|
69
|
+
expect(result2.total).toEqual(127);
|
|
70
|
+
expect(result2.breakdown).toEqual([
|
|
71
|
+
{ unitQuantity: 10, amount: 100 },
|
|
72
|
+
{ unitQuantity: 3, amount: 27 },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const result3 = calculateTierPriceGraduated(price.tiers!, 35);
|
|
76
|
+
expect(result3.total).toEqual(310);
|
|
77
|
+
expect(result3.breakdown).toEqual([
|
|
78
|
+
{ unitQuantity: 10, amount: 100 },
|
|
79
|
+
{ unitQuantity: 10, amount: 90 },
|
|
80
|
+
{ unitQuantity: 15, amount: 120 },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|