@stigg/react-sdk 5.7.0 → 5.9.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/checkout/summary/components/GraduatedPriceBreakdown.d.ts +7 -0
- package/dist/components/checkout/summary/components/LineItems.d.ts +4 -5
- package/dist/components/checkout/summary/components/getPriceBreakdownString.d.ts +12 -0
- package/dist/components/common/CollapsableSectionIcon.d.ts +5 -0
- package/dist/components/common/Typography.d.ts +2 -2
- package/dist/components/utils/numberUtils.d.ts +3 -0
- package/dist/components/utils/priceTierUtils.d.ts +2 -0
- package/dist/react-sdk.cjs.development.js +248 -89
- 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 +258 -94
- package/dist/react-sdk.esm.js.map +1 -1
- package/dist/theme/getResolvedTheme.d.ts +1 -0
- package/dist/theme/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/checkout/summary/components/GraduatedPriceBreakdown.tsx +66 -0
- package/src/components/checkout/summary/components/LineItems.tsx +65 -42
- package/src/components/checkout/summary/components/getPriceBreakdownString.ts +56 -0
- package/src/components/common/CollapsableSectionIcon.tsx +9 -0
- package/src/components/common/Typography.tsx +1 -0
- package/src/components/utils/numberUtils.ts +3 -0
- package/src/components/utils/priceTierUtils.ts +8 -0
- package/src/theme/getResolvedTheme.ts +2 -1
- package/src/theme/types.ts +1 -0
package/dist/theme/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import isNil from 'lodash/isNil';
|
|
3
|
+
import { Price, PriceTierFragment } from '@stigg/js-client-sdk';
|
|
4
|
+
import { Typography } from '../../../common/Typography';
|
|
5
|
+
import { calculateTierPriceGraduated } from '../../../utils/priceTierUtils';
|
|
6
|
+
import { formatPricePerUnit } from './getPriceBreakdownString';
|
|
7
|
+
import { LineItemContainer, LineItemRow } from './LineItems';
|
|
8
|
+
import { numberFormatter } from '../../../utils/numberUtils';
|
|
9
|
+
|
|
10
|
+
function getLabel(tiers: PriceTierFragment[], index: number): string {
|
|
11
|
+
const { unitPrice, upTo } = tiers[index];
|
|
12
|
+
if (!unitPrice) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (index === 0) {
|
|
17
|
+
return `First ${upTo}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const previousTierUpTo = tiers[index - 1].upTo || 0;
|
|
21
|
+
const startUnit = previousTierUpTo + 1;
|
|
22
|
+
|
|
23
|
+
return isNil(upTo)
|
|
24
|
+
? `${numberFormatter(startUnit)} and above`
|
|
25
|
+
: `Next ${numberFormatter(startUnit)} to ${numberFormatter(upTo)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GraduatedPriceBreakdownProps = {
|
|
29
|
+
price: Price;
|
|
30
|
+
unitQuantity: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function GraduatedPriceBreakdown({ price, unitQuantity }: GraduatedPriceBreakdownProps) {
|
|
34
|
+
const tiers = price.tiers || [];
|
|
35
|
+
|
|
36
|
+
const { breakdown } = calculateTierPriceGraduated(tiers, unitQuantity);
|
|
37
|
+
|
|
38
|
+
if (breakdown.length === 1) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{breakdown.map(({ unitQuantity, amount }, index) => (
|
|
45
|
+
<LineItemContainer>
|
|
46
|
+
<LineItemRow key={index} style={{ alignItems: 'flex-start' }}>
|
|
47
|
+
<Typography variant="body1" color="tertiary" style={{ whiteSpace: 'nowrap' }}>
|
|
48
|
+
{getLabel(tiers, index)}
|
|
49
|
+
</Typography>
|
|
50
|
+
<Typography variant="body1" color="tertiary" style={{ whiteSpace: 'nowrap' }}>
|
|
51
|
+
{formatPricePerUnit({
|
|
52
|
+
quantity: unitQuantity,
|
|
53
|
+
totalAmount: amount,
|
|
54
|
+
currency: price.currency,
|
|
55
|
+
pricingModel: price.pricingModel,
|
|
56
|
+
billingPeriod: price.billingPeriod,
|
|
57
|
+
tiers: price.tiers,
|
|
58
|
+
tiersMode: price.tiersMode,
|
|
59
|
+
})}
|
|
60
|
+
</Typography>
|
|
61
|
+
</LineItemRow>
|
|
62
|
+
</LineItemContainer>
|
|
63
|
+
))}
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,54 +1,48 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { ReactNode, useState } from 'react';
|
|
2
2
|
import styled from '@emotion/styled/macro';
|
|
3
3
|
import Grid from '@mui/material/Grid';
|
|
4
4
|
import {
|
|
5
5
|
BillingModel,
|
|
6
|
-
BillingPeriod,
|
|
7
6
|
Price,
|
|
8
7
|
SubscriptionPreviewInvoice,
|
|
9
8
|
SubscriptionPreviewV2,
|
|
9
|
+
TiersMode,
|
|
10
10
|
} from '@stigg/js-client-sdk';
|
|
11
|
+
import isEmpty from 'lodash/isEmpty';
|
|
12
|
+
import isNil from 'lodash/isNil';
|
|
13
|
+
import Link from '@mui/material/Link';
|
|
14
|
+
import { IconButton } from '@mui/material';
|
|
15
|
+
import Collapse from '@mui/material/Collapse';
|
|
11
16
|
import { Typography } from '../../../common/Typography';
|
|
12
17
|
import { currencyPriceFormatter } from '../../../utils/currencyUtils';
|
|
13
|
-
import { calculateTierPrice } from '../../../utils/priceTierUtils';
|
|
14
18
|
import { WithSkeleton } from './WithSkeleton';
|
|
15
19
|
import { Skeleton } from '../../components/Skeletons.style';
|
|
16
20
|
import { CheckoutLocalization } from '../../configurations/textOverrides';
|
|
17
21
|
import { Icon } from '../../../common/Icon';
|
|
18
22
|
import { InformationTooltip } from '../../../common/InformationTooltip';
|
|
23
|
+
import { getPriceBreakdownString } from './getPriceBreakdownString';
|
|
24
|
+
import { GraduatedPriceBreakdown } from './GraduatedPriceBreakdown';
|
|
25
|
+
import { CollapsableSectionIcon } from '../../../common/CollapsableSectionIcon';
|
|
26
|
+
import { calculateTierPrice } from '../../../utils/priceTierUtils';
|
|
19
27
|
|
|
20
28
|
export const LineItemContainer = styled.div`
|
|
21
29
|
& + & {
|
|
22
|
-
margin-top:
|
|
30
|
+
margin-top: 16px;
|
|
23
31
|
}
|
|
24
32
|
`;
|
|
25
33
|
|
|
34
|
+
export const NestedBreakdownContainer = styled.div`
|
|
35
|
+
margin-top: 16px;
|
|
36
|
+
margin-left: 16px;
|
|
37
|
+
`;
|
|
38
|
+
|
|
26
39
|
export const LineItemRow = styled.div`
|
|
27
40
|
display: flex;
|
|
28
41
|
align-items: center;
|
|
29
42
|
justify-content: space-between;
|
|
43
|
+
gap: 16px;
|
|
30
44
|
`;
|
|
31
45
|
|
|
32
|
-
export const getPriceString = ({ amount, price, quantity }: { amount: number; price: Price; quantity: number }) => {
|
|
33
|
-
const { billingPeriod } = price;
|
|
34
|
-
let billingPeriodString = null;
|
|
35
|
-
|
|
36
|
-
if (quantity) {
|
|
37
|
-
amount /= quantity;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (billingPeriod === BillingPeriod.Annually) {
|
|
41
|
-
amount /= 12;
|
|
42
|
-
billingPeriodString = '12 months';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const addonPriceFormat = currencyPriceFormatter({ amount, currency: price.currency, minimumFractionDigits: 2 });
|
|
46
|
-
|
|
47
|
-
return `${quantity > 1 ? `${quantity} x ${addonPriceFormat} each` : addonPriceFormat}${
|
|
48
|
-
billingPeriodString ? ` x ${billingPeriodString}` : ''
|
|
49
|
-
}`;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
46
|
const PayAsYouGoPriceTooltip = ({ checkoutLocalization }: { checkoutLocalization: CheckoutLocalization }) => {
|
|
53
47
|
const title = <Typography variant="body1">{checkoutLocalization.summary.payAsYouGoTooltipText}</Typography>;
|
|
54
48
|
return (
|
|
@@ -69,41 +63,70 @@ export const BilledPriceLineItem = ({
|
|
|
69
63
|
quantity: number;
|
|
70
64
|
price: Price;
|
|
71
65
|
}) => {
|
|
72
|
-
const
|
|
66
|
+
const [isNestedBreakdownOpen, setIsNestedBreakdownOpen] = useState(false);
|
|
67
|
+
const toggleNestedBreakdown = () => setIsNestedBreakdownOpen((prev) => !prev);
|
|
68
|
+
|
|
73
69
|
const isPayAsYouGo = price.pricingModel === BillingModel.UsageBased;
|
|
70
|
+
const totalAmount = price.isTieredPrice ? calculateTierPrice(price, quantity) : (price.amount || 0) * quantity;
|
|
74
71
|
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
let nestedBreakdown: ReactNode;
|
|
73
|
+
const shouldShowGraduatedPriceBreakdown =
|
|
74
|
+
price.tiersMode === TiersMode.Graduated &&
|
|
75
|
+
!!price.tiers &&
|
|
76
|
+
!isEmpty(price.tiers) &&
|
|
77
|
+
!isNil(price.tiers[0].upTo) &&
|
|
78
|
+
quantity > price.tiers[0].upTo;
|
|
79
|
+
|
|
80
|
+
if (shouldShowGraduatedPriceBreakdown) {
|
|
81
|
+
nestedBreakdown = <GraduatedPriceBreakdown price={price} unitQuantity={quantity} />;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let title: ReactNode = (
|
|
85
|
+
<Typography variant="body1" color="secondary">
|
|
86
|
+
{label}
|
|
87
|
+
</Typography>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (nestedBreakdown) {
|
|
91
|
+
title = (
|
|
92
|
+
<Link onClick={toggleNestedBreakdown} underline="none" style={{ cursor: 'pointer' }}>
|
|
93
|
+
{title}
|
|
94
|
+
</Link>
|
|
95
|
+
);
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
return (
|
|
83
99
|
<LineItemContainer>
|
|
84
100
|
<LineItemRow style={{ alignItems: 'flex-start' }}>
|
|
85
|
-
<Grid item>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{getPriceString({ amount, price, quantity })}
|
|
92
|
-
</Typography>
|
|
101
|
+
<Grid item display="flex" gap={0.5} style={{ whiteSpace: 'nowrap' }}>
|
|
102
|
+
{title}
|
|
103
|
+
{nestedBreakdown && (
|
|
104
|
+
<IconButton onClick={toggleNestedBreakdown} sx={{ padding: 0 }}>
|
|
105
|
+
<CollapsableSectionIcon $isOpen={isNestedBreakdownOpen} $size={16} />
|
|
106
|
+
</IconButton>
|
|
93
107
|
)}
|
|
94
108
|
</Grid>
|
|
95
109
|
<Grid item display="flex" gap={1} alignItems="center">
|
|
96
110
|
{isPayAsYouGo && <PayAsYouGoPriceTooltip checkoutLocalization={checkoutLocalization} />}
|
|
97
|
-
<Typography variant="body1" color="secondary" style={{
|
|
98
|
-
{
|
|
99
|
-
|
|
111
|
+
<Typography variant="body1" color="secondary" style={{ whiteSpace: 'nowrap' }}>
|
|
112
|
+
{getPriceBreakdownString({
|
|
113
|
+
totalAmount,
|
|
114
|
+
quantity,
|
|
100
115
|
currency: price.currency,
|
|
101
|
-
|
|
116
|
+
pricingModel: price.pricingModel,
|
|
117
|
+
billingPeriod: price.billingPeriod,
|
|
118
|
+
tiers: price.tiers,
|
|
119
|
+
tiersMode: price.tiersMode,
|
|
102
120
|
})}
|
|
103
121
|
{isPayAsYouGo && ' / unit'}
|
|
104
122
|
</Typography>
|
|
105
123
|
</Grid>
|
|
106
124
|
</LineItemRow>
|
|
125
|
+
{nestedBreakdown && (
|
|
126
|
+
<Collapse in={isNestedBreakdownOpen}>
|
|
127
|
+
<NestedBreakdownContainer>{nestedBreakdown}</NestedBreakdownContainer>
|
|
128
|
+
</Collapse>
|
|
129
|
+
)}
|
|
107
130
|
</LineItemContainer>
|
|
108
131
|
);
|
|
109
132
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BillingModel, BillingPeriod, Currency, Price, TiersMode } from '@stigg/js-client-sdk';
|
|
2
|
+
import { currencyPriceFormatter } from '../../../utils/currencyUtils';
|
|
3
|
+
import { isBulkTiers, isQuantityInFirstTier } from '../../../utils/priceTierUtils';
|
|
4
|
+
import { numberFormatter } from '../../../utils/numberUtils';
|
|
5
|
+
|
|
6
|
+
export type GetPriceBreakdownStringProps = {
|
|
7
|
+
totalAmount: number;
|
|
8
|
+
quantity: number;
|
|
9
|
+
tiersMode: Price['tiersMode'];
|
|
10
|
+
tiers: Price['tiers'];
|
|
11
|
+
currency: Currency;
|
|
12
|
+
pricingModel: BillingModel;
|
|
13
|
+
billingPeriod: BillingPeriod;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function formatPricePerUnit({
|
|
17
|
+
quantity,
|
|
18
|
+
totalAmount,
|
|
19
|
+
pricingModel,
|
|
20
|
+
billingPeriod,
|
|
21
|
+
currency,
|
|
22
|
+
}: GetPriceBreakdownStringProps) {
|
|
23
|
+
const isPerUnit = pricingModel === BillingModel.PerUnit;
|
|
24
|
+
const featureUnits = quantity && (isPerUnit || quantity > 1) ? `${numberFormatter(quantity)} x ` : '';
|
|
25
|
+
const billingPeriodString = billingPeriod === BillingPeriod.Annually ? ' x 12 months' : '';
|
|
26
|
+
|
|
27
|
+
const unitPrice = totalAmount / quantity / (billingPeriod === BillingPeriod.Annually ? 12 : 1);
|
|
28
|
+
const formattedUnitPrice = currencyPriceFormatter({
|
|
29
|
+
amount: unitPrice,
|
|
30
|
+
currency,
|
|
31
|
+
minimumFractionDigits: 2,
|
|
32
|
+
});
|
|
33
|
+
const formattedTotalPrice = currencyPriceFormatter({
|
|
34
|
+
amount: totalAmount,
|
|
35
|
+
currency,
|
|
36
|
+
minimumFractionDigits: 2,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return `${featureUnits}${formattedUnitPrice}${billingPeriodString} ${
|
|
40
|
+
billingPeriodString || featureUnits ? ` = ${formattedTotalPrice}` : ''
|
|
41
|
+
}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getPriceBreakdownString(params: GetPriceBreakdownStringProps) {
|
|
45
|
+
const { totalAmount, quantity, tiersMode, tiers, currency } = params;
|
|
46
|
+
if (isBulkTiers(tiers) || (tiersMode === TiersMode.Graduated && !isQuantityInFirstTier(tiers, quantity))) {
|
|
47
|
+
const formattedTotalPrice = currencyPriceFormatter({
|
|
48
|
+
amount: totalAmount,
|
|
49
|
+
currency,
|
|
50
|
+
minimumFractionDigits: 2,
|
|
51
|
+
});
|
|
52
|
+
return `${numberFormatter(quantity)} for ${formattedTotalPrice}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return formatPricePerUnit(params);
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ChevronRight } from 'react-feather';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
export const CollapsableSectionIcon = styled(ChevronRight)<{ $isOpen: boolean; $size?: number }>`
|
|
5
|
+
height: ${({ $size = 24 }) => `${$size}px`};
|
|
6
|
+
width: ${({ $size = 24 }) => `${$size}px`};
|
|
7
|
+
transition: all 0.2s ease-in;
|
|
8
|
+
${({ $isOpen }) => $isOpen && `transform: rotate(90deg)`}
|
|
9
|
+
`;
|
|
@@ -123,6 +123,14 @@ export function hasTierWithUnitPrice(tiers: PriceTierFragment[] | null | undefin
|
|
|
123
123
|
return tiers?.some(({ unitPrice, upTo }) => unitPrice && !isNil(upTo));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export function isBulkTiers(tiers: PriceTierFragment[] | null | undefined) {
|
|
127
|
+
return tiers?.every(({ unitPrice, upTo }) => !unitPrice || isNil(upTo));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isQuantityInFirstTier(tiers: PriceTierFragment[] | null | undefined, quantity: number) {
|
|
131
|
+
return tiers?.[0].upTo && quantity <= tiers[0].upTo;
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
export function getTiersPerUnitQuantities(
|
|
127
135
|
plan: PaywallPlan,
|
|
128
136
|
billingPeriod: BillingPeriod,
|
|
@@ -41,7 +41,8 @@ export const getResolvedTheme = (customizedTheme?: CustomizedTheme): StiggTheme
|
|
|
41
41
|
text: {
|
|
42
42
|
primary: textColor.hex(),
|
|
43
43
|
secondary: textColor.alpha(0.75).toString(),
|
|
44
|
-
|
|
44
|
+
tertiary: textColor.alpha(0.5).toString(),
|
|
45
|
+
disabled: textColor.alpha(0.35).toString(),
|
|
45
46
|
},
|
|
46
47
|
},
|
|
47
48
|
layout: {
|