@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.
@@ -24,6 +24,7 @@ export declare const getResolvedTheme: (customizedTheme?: {
24
24
  text?: {
25
25
  primary?: string | undefined;
26
26
  secondary?: string | undefined;
27
+ tertiary?: string | undefined;
27
28
  disabled?: string | undefined;
28
29
  } | undefined;
29
30
  } | undefined;
@@ -29,6 +29,7 @@ export declare type StiggTheme = {
29
29
  text: {
30
30
  primary: string;
31
31
  secondary: string;
32
+ tertiary: string;
32
33
  disabled: string;
33
34
  };
34
35
  };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.7.0",
2
+ "version": "5.9.0",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -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: 8px;
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 { billingPeriod } = price;
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 amount;
76
- if (price.isTieredPrice) {
77
- amount = calculateTierPrice(price, quantity);
78
- } else {
79
- amount = price.amount! * quantity;
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
- <Typography variant="body1" color="secondary">
87
- {label}
88
- </Typography>
89
- {(quantity > 1 || billingPeriod === BillingPeriod.Annually) && (
90
- <Typography variant="body1" color="secondary">
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={{ wordBreak: 'break-word' }}>
98
- {currencyPriceFormatter({
99
- amount,
111
+ <Typography variant="body1" color="secondary" style={{ whiteSpace: 'nowrap' }}>
112
+ {getPriceBreakdownString({
113
+ totalAmount,
114
+ quantity,
100
115
  currency: price.currency,
101
- minimumFractionDigits: 2,
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
+ `;
@@ -9,6 +9,7 @@ type Colors =
9
9
  | 'primary.main'
10
10
  | 'primary.main.light'
11
11
  | 'secondary'
12
+ | 'tertiary'
12
13
  | 'disabled'
13
14
  | 'white'
14
15
  | 'warning'
@@ -0,0 +1,3 @@
1
+ export function numberFormatter(number: number, { locate }: { locate?: string } = {}) {
2
+ return new Intl.NumberFormat(locate).format(number);
3
+ }
@@ -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
- disabled: textColor.alpha(0.5).toString(),
44
+ tertiary: textColor.alpha(0.5).toString(),
45
+ disabled: textColor.alpha(0.35).toString(),
45
46
  },
46
47
  },
47
48
  layout: {
@@ -29,6 +29,7 @@ export type StiggTheme = {
29
29
  text: {
30
30
  primary: string;
31
31
  secondary: string;
32
+ tertiary: string;
32
33
  disabled: string;
33
34
  };
34
35
  };