@stigg/react-sdk 4.13.1 → 4.14.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.
Files changed (34) hide show
  1. package/dist/components/common/TiersSelectContainer.d.ts +8 -3
  2. package/dist/components/common/VolumeBulkSelect.d.ts +5 -0
  3. package/dist/components/common/VolumePerUnitInput.d.ts +5 -0
  4. package/dist/components/paywall/PlanOfferingButton.d.ts +2 -1
  5. package/dist/components/paywall/PlanPrice.d.ts +3 -1
  6. package/dist/components/paywall/index.d.ts +1 -1
  7. package/dist/components/paywall/paywallTextOverrides.d.ts +9 -5
  8. package/dist/components/utils/getPaidPriceText.d.ts +2 -1
  9. package/dist/components/utils/getPlanPrice.d.ts +1 -1
  10. package/dist/components/utils/priceTierUtils.d.ts +3 -2
  11. package/dist/index.d.ts +1 -1
  12. package/dist/react-sdk.cjs.development.js +558 -365
  13. package/dist/react-sdk.cjs.development.js.map +1 -1
  14. package/dist/react-sdk.cjs.production.min.js +1 -1
  15. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  16. package/dist/react-sdk.esm.js +628 -430
  17. package/dist/react-sdk.esm.js.map +1 -1
  18. package/package.json +2 -2
  19. package/src/components/checkout/hooks/usePlanStepModel.ts +3 -3
  20. package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +10 -7
  21. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +10 -2
  22. package/src/components/common/TiersSelectContainer.tsx +33 -62
  23. package/src/components/common/VolumeBulkSelect.tsx +68 -0
  24. package/src/components/common/VolumePerUnitInput.tsx +40 -0
  25. package/src/components/paywall/PlanOffering.tsx +15 -6
  26. package/src/components/paywall/PlanOfferingButton.tsx +8 -2
  27. package/src/components/paywall/PlanPrice.tsx +20 -0
  28. package/src/components/paywall/index.ts +1 -1
  29. package/src/components/paywall/paywallTextOverrides.ts +7 -2
  30. package/src/components/utils/getPaidPriceText.ts +9 -1
  31. package/src/components/utils/getPlanPrice.ts +6 -1
  32. package/src/components/utils/priceTierUtils.ts +60 -14
  33. package/src/index.ts +1 -0
  34. package/src/stories/mocks/checkout/mockCheckoutPreview.ts +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.13.1",
2
+ "version": "4.14.0",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -112,7 +112,7 @@
112
112
  "@emotion/react": "^11.10.5",
113
113
  "@emotion/styled": "^11.10.5",
114
114
  "@mui/material": "^5.12.0",
115
- "@stigg/js-client-sdk": "2.24.6",
115
+ "@stigg/js-client-sdk": "2.25.0",
116
116
  "@stripe/react-stripe-js": "^2.1.1",
117
117
  "@stripe/stripe-js": "^1.54.1",
118
118
  "@types/styled-components": "^5.1.26",
@@ -1,6 +1,6 @@
1
1
  import keyBy from 'lodash/keyBy';
2
2
  import compact from 'lodash/compact';
3
- import { BillableFeatureInput, BillingPeriod } from '@stigg/api-client-js/src/generated/sdk';
3
+ import { BillableFeatureInput, BillingPeriod, TiersMode } from '@stigg/api-client-js/src/generated/sdk';
4
4
  import { BillableFeature, BillingModel, Plan, Price, Subscription } from '@stigg/js-client-sdk';
5
5
  import { useCheckoutContext } from '../CheckoutProvider';
6
6
  import { getTierByQuantity } from '../../utils/priceTierUtils';
@@ -73,9 +73,9 @@ function getBillableFeatures(
73
73
 
74
74
  quantity = getValidPriceQuantity(price, preconfiguredQuantity || 1);
75
75
 
76
- if (price.isTieredPrice) {
76
+ if (price.isTieredPrice && price.tiersMode === TiersMode.Volume) {
77
77
  const tier = getTierByQuantity(price.tiers!, quantity);
78
- quantity = tier!.upTo;
78
+ quantity = tier!.upTo!;
79
79
  }
80
80
 
81
81
  return {
@@ -6,6 +6,8 @@ type StripeElementsProps = {
6
6
  elements: StripeElements | null;
7
7
  };
8
8
 
9
+ const FAILED_PAYMENT_COLLECTION_STATUSES = [PaymentCollection.Failed, PaymentCollection.Processing];
10
+
9
11
  export async function handleStripeFormValidations({ elements }: Pick<StripeElementsProps, 'elements'>) {
10
12
  if (!elements) {
11
13
  const errorMessage = 'Stripe elements not initialized';
@@ -30,19 +32,20 @@ export async function handleStripeNextAction({
30
32
  applySubscriptionResults: ApplySubscriptionResults;
31
33
  stripe: Stripe | null;
32
34
  }) {
33
- const paymentIntentClientSecret = applySubscriptionResults?.subscription?.latestInvoice?.paymentSecret;
34
- if (
35
- stripe &&
36
- paymentIntentClientSecret &&
37
- applySubscriptionResults?.subscription?.paymentCollection === PaymentCollection.ActionRequired
38
- ) {
35
+ const subscription = applySubscriptionResults?.subscription;
36
+ const paymentIntentClientSecret = subscription?.latestInvoice?.paymentSecret;
37
+ const paymentCollection = subscription?.paymentCollection;
38
+
39
+ if (stripe && paymentIntentClientSecret && paymentCollection === PaymentCollection.ActionRequired) {
39
40
  const { error: NextActionError } = await stripe.handleNextAction({ clientSecret: paymentIntentClientSecret });
40
41
  if (NextActionError) {
41
42
  return { errorMessage: NextActionError.message || '' };
42
43
  }
44
+ } else if (paymentCollection && FAILED_PAYMENT_COLLECTION_STATUSES.includes(paymentCollection)) {
45
+ return { errorMessage: subscription?.latestInvoice?.errorMessage || 'An error occurred - try again later' };
43
46
  }
44
47
 
45
- return { subscription: applySubscriptionResults?.subscription };
48
+ return { subscription };
46
49
  }
47
50
 
48
51
  async function handleStripeSetup({
@@ -103,7 +103,7 @@ export function PlanCharge({
103
103
  if (isPayAsYouGo) {
104
104
  chargeRow += ' / unit';
105
105
  }
106
- } else if (charge.isTieredPrice && featureId) {
106
+ } else if (charge.isTieredPrice && charge.tiersMode && featureId) {
107
107
  const tier = getTierByQuantity(charge.tiers!, billableFeature!.quantity || 1);
108
108
  chargeRow = (
109
109
  <TiersSelectContainer
@@ -111,8 +111,16 @@ export function PlanCharge({
111
111
  tiers={charge.tiers}
112
112
  selectedTier={tier}
113
113
  handleTierChange={(tier: PriceTierFragment) => {
114
- setBillableFeature(featureId, tier.upTo);
114
+ if (tier.upTo) {
115
+ setBillableFeature(featureId, tier.upTo);
116
+ }
115
117
  }}
118
+ tiersMode={charge.tiersMode}
119
+ perUnitQuantity={billableFeature?.quantity}
120
+ handlePerUnitQuantityChange={(quantity: number) => {
121
+ setBillableFeature(featureId, quantity);
122
+ }}
123
+ width={120}
116
124
  />
117
125
  );
118
126
  } else {
@@ -1,78 +1,49 @@
1
- import { PriceTierFragment } from '@stigg/js-client-sdk';
2
- import MenuItem from '@mui/material/MenuItem';
3
- import OutlinedInput from '@mui/material/OutlinedInput';
4
- import Select, { SelectChangeEvent } from '@mui/material/Select';
5
- import { map } from 'lodash';
6
- import React, { ReactNode } from 'react';
1
+ import { PriceTierFragment, TiersMode } from '@stigg/js-client-sdk';
2
+ import React from 'react';
7
3
  import styled from '@emotion/styled/macro';
8
4
  import { getTierByQuantity } from '../utils/priceTierUtils';
9
5
  import { Typography } from './Typography';
6
+ import { VolumePerUnitInput } from './VolumePerUnitInput';
7
+ import { VolumeBulkSelect } from './VolumeBulkSelect';
10
8
 
11
- const TierSelect = styled(Select)`
12
- border-radius: 10px;
13
- min-height: 42px;
14
- min-width: 120px;
15
-
16
- &:hover .MuiOutlinedInput-notchedOutline {
17
- border-color: ${({ theme }) => theme.stigg.palette.outlinedBorder};
18
- }
19
- `;
20
-
21
- const TierInput = styled(OutlinedInput)`
22
- & .MuiInputBase-input {
23
- padding: 10px 12px;
24
- }
25
-
26
- &.Mui-focused .MuiOutlinedInput-notchedOutline {
27
- border-color: ${({ theme }) => theme.stigg.palette.primary} !important;
28
- }
29
- `;
30
-
31
- export function TiersSelectContainer({
32
- componentId,
33
- tiers,
34
- tierUnits,
35
- selectedTier,
36
- handleTierChange,
37
- }: {
9
+ export type TiersSelectContainerProps = {
38
10
  componentId: string;
39
11
  tiers?: PriceTierFragment[] | null;
40
12
  tierUnits?: string;
41
13
  selectedTier?: PriceTierFragment;
42
14
  handleTierChange: (tier: PriceTierFragment) => void;
43
- }) {
44
- const handleChange = (event: SelectChangeEvent<unknown>, _: ReactNode) => {
15
+ tiersMode?: TiersMode | null;
16
+ handlePerUnitQuantityChange: (quantity: number) => void;
17
+ perUnitQuantity?: number;
18
+ width?: number;
19
+ };
20
+
21
+ const TiersSelectLayout = styled(Typography)`
22
+ min-height: 46px;
23
+ `;
24
+
25
+ export function TiersSelectContainer(props: TiersSelectContainerProps) {
26
+ const { tiers, tiersMode, handleTierChange } = props;
27
+ const handleChange = (quantity: number) => {
45
28
  if (!tiers) return;
46
29
 
47
- handleTierChange(getTierByQuantity(tiers, event.target.value as number)!);
30
+ handleTierChange(getTierByQuantity(tiers, quantity)!);
31
+ };
32
+
33
+ const getContent = () => {
34
+ switch (tiersMode) {
35
+ case TiersMode.VolumePerUnit:
36
+ return <VolumePerUnitInput handleChange={handleChange} {...props} />;
37
+ case TiersMode.Volume:
38
+ return <VolumeBulkSelect handleChange={handleChange} {...props} />;
39
+ default:
40
+ return <div />;
41
+ }
48
42
  };
49
43
 
50
44
  return (
51
- <Typography as="div" className="stigg-price-tier-select" style={{ minHeight: '46px' }}>
52
- {tiers ? (
53
- <TierSelect
54
- value={selectedTier ? selectedTier.upTo.toString() : tiers[0].upTo.toString()}
55
- fullWidth
56
- onChange={handleChange}
57
- id={componentId}
58
- input={<TierInput />}
59
- MenuProps={{
60
- MenuListProps: { disablePadding: true },
61
- PaperProps: {
62
- sx: { marginTop: '4px', borderRadius: '10px' },
63
- },
64
- }}>
65
- {map(tiers, (tier: PriceTierFragment) => (
66
- <MenuItem className="stigg-price-tier-menu-item-text" key={tier.upTo} value={tier.upTo.toString()}>
67
- <Typography variant="body1" color="primary" style={{ lineHeight: 'unset' }}>
68
- {tier.upTo} {tierUnits}
69
- </Typography>
70
- </MenuItem>
71
- ))}
72
- </TierSelect>
73
- ) : (
74
- <div />
75
- )}
76
- </Typography>
45
+ <TiersSelectLayout as="div" className="stigg-price-tier-select">
46
+ {getContent()}
47
+ </TiersSelectLayout>
77
48
  );
78
49
  }
@@ -0,0 +1,68 @@
1
+ import { PriceTierFragment } from '@stigg/js-client-sdk';
2
+ import styled from '@emotion/styled/macro';
3
+ import OutlinedInput from '@mui/material/OutlinedInput';
4
+ import Select from '@mui/material/Select';
5
+ import MenuItem from '@mui/material/MenuItem';
6
+ import { map } from 'lodash';
7
+ import React from 'react';
8
+ import { Typography } from './Typography';
9
+ import { TiersSelectContainerProps } from './TiersSelectContainer';
10
+
11
+ const TierSelect = styled(Select)`
12
+ border-radius: 10px;
13
+ min-height: 42px;
14
+ min-width: 120px;
15
+
16
+ &:hover .MuiOutlinedInput-notchedOutline {
17
+ border-color: ${({ theme }) => theme.stigg.palette.outlinedBorder};
18
+ }
19
+ `;
20
+
21
+ const TierInput = styled(OutlinedInput)`
22
+ & .MuiInputBase-input {
23
+ padding: 10px 12px;
24
+ }
25
+
26
+ &.Mui-focused .MuiOutlinedInput-notchedOutline {
27
+ border-color: ${({ theme }) => theme.stigg.palette.primary} !important;
28
+ }
29
+ `;
30
+
31
+ export function VolumeBulkSelect({
32
+ selectedTier,
33
+ componentId,
34
+ tierUnits,
35
+ tiers,
36
+ handleChange,
37
+ }: Pick<TiersSelectContainerProps, 'selectedTier' | 'componentId' | 'tiers' | 'tierUnits'> & {
38
+ handleChange: (quantity: number) => void;
39
+ }) {
40
+ if (!tiers) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <TierSelect
46
+ value={selectedTier ? selectedTier.upTo?.toString() : tiers[0].upTo?.toString()}
47
+ fullWidth
48
+ onChange={(event) => {
49
+ handleChange(event.target.value as number);
50
+ }}
51
+ id={componentId}
52
+ input={<TierInput />}
53
+ MenuProps={{
54
+ MenuListProps: { disablePadding: true },
55
+ PaperProps: {
56
+ sx: { marginTop: '4px', borderRadius: '10px' },
57
+ },
58
+ }}>
59
+ {map(tiers, (tier: PriceTierFragment) => (
60
+ <MenuItem className="stigg-price-tier-menu-item-text" key={tier.upTo} value={tier.upTo?.toString()}>
61
+ <Typography variant="body1" color="primary" style={{ lineHeight: 'unset' }}>
62
+ {tier.upTo} {tierUnits}
63
+ </Typography>
64
+ </MenuItem>
65
+ ))}
66
+ </TierSelect>
67
+ );
68
+ }
@@ -0,0 +1,40 @@
1
+ import InputAdornment from '@mui/material/InputAdornment';
2
+ import React from 'react';
3
+ import { Typography } from './Typography';
4
+ import { InputField } from '../checkout/components';
5
+ import { TiersSelectContainerProps } from './TiersSelectContainer';
6
+
7
+ export function VolumePerUnitInput({
8
+ width,
9
+ tierUnits,
10
+ perUnitQuantity,
11
+ handlePerUnitQuantityChange,
12
+ handleChange,
13
+ }: Pick<TiersSelectContainerProps, 'width' | 'tierUnits' | 'perUnitQuantity' | 'handlePerUnitQuantityChange'> & {
14
+ handleChange: (quantity: number) => void;
15
+ }) {
16
+ return (
17
+ <InputField
18
+ type="number"
19
+ fullWidth
20
+ sx={{ minHeight: '46px', width }}
21
+ InputProps={{
22
+ endAdornment: (
23
+ <InputAdornment position="end">
24
+ <Typography variant="body1" color="primary">
25
+ {tierUnits}
26
+ </Typography>
27
+ </InputAdornment>
28
+ ),
29
+ }}
30
+ value={perUnitQuantity || 1}
31
+ onChange={(event) => {
32
+ const quantity = event?.target?.value ? Number(event?.target?.value) : null;
33
+ if (quantity) {
34
+ handleChange(quantity);
35
+ handlePerUnitQuantityChange(quantity);
36
+ }
37
+ }}
38
+ />
39
+ );
40
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import {
3
3
  BillableFeature,
4
4
  BillingPeriod,
@@ -18,7 +18,7 @@ import { flexLayoutMapper } from '../../theme/getResolvedTheme';
18
18
  import { Typography } from '../common/Typography';
19
19
  import MiniSchedule from '../../assets/mini-schedule.svg';
20
20
  import { PlanPrice } from './PlanPrice';
21
- import { getSelectedTier } from '../utils/priceTierUtils';
21
+ import { getSelectedTier, getSelectedTierQuantityBeFeature } from '../utils/priceTierUtils';
22
22
 
23
23
  const PlanOfferingButtonHeight = '66px';
24
24
 
@@ -167,22 +167,28 @@ export function PlanOffering({
167
167
  );
168
168
  }
169
169
 
170
- const [selectedTierByFeature, setSelectedTierByFeature] = React.useState<Record<string, PriceTierFragment>>(
170
+ const [perUnitQuantityByFeature, setPerUnitQuantityByFeature] = useState<Record<string, number>>({});
171
+ const [selectedTierByFeature, setSelectedTierByFeature] = useState<Record<string, PriceTierFragment>>(
171
172
  getSelectedTier(plan, billingPeriod, currentSubscription, {}, selectDefaultTierIndex),
172
173
  );
173
174
 
174
175
  useEffect(() => {
175
176
  setSelectedTierByFeature(getSelectedTier(plan, billingPeriod, currentSubscription, selectedTierByFeature));
177
+ setPerUnitQuantityByFeature(getSelectedTierQuantityBeFeature(plan, billingPeriod, currentSubscription));
176
178
  // eslint-disable-next-line react-hooks/exhaustive-deps
177
179
  }, [billingPeriod, currentSubscription, plan]);
178
180
 
179
181
  const onPlanButtonClick = (intentionType: SubscribeIntentionType) => {
180
- const billableFeatures: BillableFeature[] = Object.keys(selectedTierByFeature).map((featureId) => ({
182
+ const tierBillableFeatures: BillableFeature[] = Object.keys(selectedTierByFeature).map((featureId) => ({
181
183
  featureId,
182
- quantity: selectedTierByFeature[featureId].upTo,
184
+ quantity: selectedTierByFeature[featureId].upTo || 0,
185
+ }));
186
+ const perUnitBillableFeatures: BillableFeature[] = Object.keys(perUnitQuantityByFeature).map((featureId) => ({
187
+ featureId,
188
+ quantity: perUnitQuantityByFeature[featureId],
183
189
  }));
184
190
 
185
- return onPlanSelected(intentionType, billableFeatures);
191
+ return onPlanSelected(intentionType, { ...tierBillableFeatures, ...perUnitBillableFeatures });
186
192
  };
187
193
 
188
194
  return (
@@ -218,6 +224,8 @@ export function PlanOffering({
218
224
  locale={locale}
219
225
  hasAnnuallyPrice={hasAnnuallyPrice}
220
226
  hasMonthlyPrice={hasMonthlyPrice}
227
+ setPerUnitQuantityByFeature={setPerUnitQuantityByFeature}
228
+ perUnitQuantityByFeature={perUnitQuantityByFeature}
221
229
  />
222
230
 
223
231
  {showCTAButton ? (
@@ -232,6 +240,7 @@ export function PlanOffering({
232
240
  paywallLocale={paywallLocale}
233
241
  withTrialLeftRow={withTrialLeftRow}
234
242
  selectedTierByFeature={selectedTierByFeature}
243
+ perUnitQuantityByFeature={perUnitQuantityByFeature}
235
244
  />
236
245
  ) : (
237
246
  <div style={{ height: PlanOfferingButtonHeight }} />
@@ -83,6 +83,7 @@ type PlanOfferingButtonProps = {
83
83
  onPlanSelected: (intentionType: SubscribeIntentionType) => void | Promise<void>;
84
84
  withTrialLeftRow: boolean;
85
85
  selectedTierByFeature: Record<string, PriceTierFragment>;
86
+ perUnitQuantityByFeature: Record<string, number>;
86
87
  };
87
88
 
88
89
  export function PlanOfferingButton({
@@ -96,6 +97,7 @@ export function PlanOfferingButton({
96
97
  withTrialLeftRow,
97
98
  currentSubscription,
98
99
  selectedTierByFeature,
100
+ perUnitQuantityByFeature,
99
101
  }: PlanOfferingButtonProps) {
100
102
  const theme = useTheme();
101
103
  const [isLoading, setIsLoading] = useState(false);
@@ -142,7 +144,11 @@ export function PlanOfferingButton({
142
144
  isSameBillingPeriod ||
143
145
  (plan.pricingType && [PricingType.Free, PricingType.Custom].includes(plan.pricingType))
144
146
  ) {
145
- const planComparison = compareSelectedTierToCurrentTier(selectedTierByFeature, currentSubscription);
147
+ const planComparison = compareSelectedTierToCurrentTier(
148
+ selectedTierByFeature,
149
+ perUnitQuantityByFeature,
150
+ currentSubscription,
151
+ );
146
152
  switch (planComparison) {
147
153
  case PriceTierComparison.Lower:
148
154
  buttonProps.intentionType = SubscribeIntentionType.CHANGE_UNIT_QUANTITY;
@@ -163,7 +169,7 @@ export function PlanOfferingButton({
163
169
  buttonProps.intentionType = SubscribeIntentionType.CHANGE_BILLING_PERIOD;
164
170
  }
165
171
  } else if (isCustomPrice) {
166
- buttonProps.title = custom;
172
+ buttonProps.title = typeof custom === 'function' ? custom({ plan, selectedBillingPeriod: billingPeriod }) : custom;
167
173
  buttonProps.intentionType = SubscribeIntentionType.REQUEST_CUSTOM_PLAN_ACCESS;
168
174
  }
169
175
 
@@ -65,6 +65,8 @@ export const PlanPrice = ({
65
65
  locale,
66
66
  hasMonthlyPrice,
67
67
  hasAnnuallyPrice,
68
+ perUnitQuantityByFeature,
69
+ setPerUnitQuantityByFeature,
68
70
  }: {
69
71
  showStartingAt: boolean;
70
72
  withUnitPriceRow: boolean;
@@ -72,6 +74,8 @@ export const PlanPrice = ({
72
74
  withTiersRow: boolean;
73
75
  selectedTierByFeature: Record<string, PriceTierFragment>;
74
76
  setSelectedTierByFeature: React.Dispatch<React.SetStateAction<Record<string, PriceTierFragment>>>;
77
+ perUnitQuantityByFeature: Record<string, number>;
78
+ setPerUnitQuantityByFeature: React.Dispatch<React.SetStateAction<Record<string, number>>>;
75
79
  plan: PaywallPlan;
76
80
  billingPeriod: BillingPeriod;
77
81
  paywallLocale: PaywallLocalization;
@@ -86,6 +90,7 @@ export const PlanPrice = ({
86
90
  locale,
87
91
  hasMonthlyPrice,
88
92
  selectedTierByFeature,
93
+ perUnitQuantityByFeature,
89
94
  );
90
95
 
91
96
  // We currently only support prices with one tier - so we select the first one
@@ -94,6 +99,7 @@ export const PlanPrice = ({
94
99
  });
95
100
  const featureId = tieredPrice ? tieredPrice.feature!.featureId : undefined;
96
101
  const selectedTier = featureId ? selectedTierByFeature[featureId] : undefined;
102
+ const perUnitQuantity = featureId ? perUnitQuantityByFeature[featureId] : undefined;
97
103
 
98
104
  const handleTierChange = (tier: PriceTierFragment) => {
99
105
  if (!featureId) {
@@ -106,6 +112,17 @@ export const PlanPrice = ({
106
112
  }));
107
113
  };
108
114
 
115
+ const handlePerUnitQuantityChange = (quantity: number) => {
116
+ if (!featureId) {
117
+ return;
118
+ }
119
+
120
+ setPerUnitQuantityByFeature((prevState) => ({
121
+ ...prevState,
122
+ [featureId]: quantity,
123
+ }));
124
+ };
125
+
109
126
  return (
110
127
  <PlanPriceContainer as="div" className="stigg-price-text">
111
128
  <>
@@ -146,6 +163,9 @@ export const PlanPrice = ({
146
163
  tierUnits={tierUnits}
147
164
  selectedTier={selectedTier}
148
165
  handleTierChange={handleTierChange}
166
+ tiersMode={tieredPrice?.tiersMode}
167
+ handlePerUnitQuantityChange={handlePerUnitQuantityChange}
168
+ perUnitQuantity={perUnitQuantity}
149
169
  />
150
170
  ) : null}
151
171
  </>
@@ -1,3 +1,3 @@
1
1
  export { PaywallContainer as Paywall, PaywallContainerProps as PaywallProps } from './PaywallContainer';
2
2
  export { PaywallPlan, OnPlanSelectedCallbackFn, SubscribeIntentionType } from './types';
3
- export { PaywallLocalization, PlanPriceText } from './paywallTextOverrides';
3
+ export { PaywallLocalization, PlanPriceText, CurrentPlanParams } from './paywallTextOverrides';
@@ -13,6 +13,11 @@ import { PaywallPlan } from './types';
13
13
 
14
14
  export { PlanPriceText } from '../utils/getPlanPrice';
15
15
 
16
+ export type CurrentPlanParams = {
17
+ plan: Plan;
18
+ selectedBillingPeriod: BillingPeriod;
19
+ };
20
+
16
21
  export type PaywallLocalization = {
17
22
  highlightChip: string;
18
23
  entitlementsTitle?: (plan: PaywallPlan) => string;
@@ -20,7 +25,7 @@ export type PaywallLocalization = {
20
25
  upgrade: string | ((plan: PaywallPlan) => string);
21
26
  downgrade: string | ((plan: PaywallPlan) => string);
22
27
  startTrial: (plan: PaywallPlan) => string;
23
- custom: string;
28
+ custom: ((data: CurrentPlanParams) => string) | string;
24
29
  currentPlan: string;
25
30
  startNew: string;
26
31
  switchToBillingPeriod: (billingPeriod: BillingPeriod) => string;
@@ -30,7 +35,7 @@ export type PaywallLocalization = {
30
35
  startingAtCaption: string;
31
36
  billingPeriod?: (billingPeriod: BillingPeriod) => string;
32
37
  pricePeriod: (billingPeriod: BillingPeriod) => string;
33
- custom: string;
38
+ custom: ((data: CurrentPlanParams) => string) | string;
34
39
  priceNotSet: string;
35
40
  free: PlanPriceText | ((currency?: PaywallCurrency) => PlanPriceText);
36
41
  paid?: (priceData: {
@@ -18,6 +18,7 @@ type GetPaidPriceTextParams = {
18
18
  shouldShowMonthlyPriceAmount: boolean;
19
19
  selectedTierByFeature: Record<string, PriceTierFragment>;
20
20
  paywallLocale: PaywallLocalization;
21
+ perUnitQuantityByFeature?: Record<string, number>;
21
22
  };
22
23
 
23
24
  export function getPaidPriceText({
@@ -28,6 +29,7 @@ export function getPaidPriceText({
28
29
  shouldShowMonthlyPriceAmount,
29
30
  selectedTierByFeature,
30
31
  paywallLocale,
32
+ perUnitQuantityByFeature,
31
33
  }: GetPaidPriceTextParams): PlanPriceText {
32
34
  const { amount, currency } = paywallCalculatedPrice || planPrices[0];
33
35
  const priceAmount = amount || 0;
@@ -53,7 +55,13 @@ export function getPaidPriceText({
53
55
  tierUnits = getPriceFeatureUnit(price);
54
56
  }
55
57
 
56
- priceNumber += calculateTierPrice(price, currentTier, selectedBillingPeriod, shouldShowMonthlyPriceAmount);
58
+ priceNumber += calculateTierPrice(
59
+ price,
60
+ currentTier,
61
+ selectedBillingPeriod,
62
+ shouldShowMonthlyPriceAmount,
63
+ perUnitQuantityByFeature?.[price.feature!.featureId],
64
+ );
57
65
  }
58
66
  }
59
67
 
@@ -16,6 +16,7 @@ export function getPlanPrice(
16
16
  locale: string,
17
17
  shouldShowMonthlyPriceAmount: boolean,
18
18
  selectedTierByFeature?: Record<string, PriceTierFragment>,
19
+ perUnitQuantityByFeature?: Record<string, number>,
19
20
  ): PlanPriceText {
20
21
  switch (plan.pricingType) {
21
22
  case PricingType.Free:
@@ -26,7 +27,10 @@ export function getPlanPrice(
26
27
  return paywallLocale.price.free;
27
28
  case PricingType.Custom:
28
29
  return {
29
- price: paywallLocale.price.custom,
30
+ price:
31
+ typeof paywallLocale.price.custom === 'function'
32
+ ? paywallLocale.price.custom({ plan, selectedBillingPeriod: billingPeriod })
33
+ : paywallLocale.price.custom,
30
34
  };
31
35
  case PricingType.Paid: {
32
36
  const planPrices = plan.pricePoints.filter((pricePoint) => pricePoint.billingPeriod === billingPeriod);
@@ -45,6 +49,7 @@ export function getPlanPrice(
45
49
  paywallCalculatedPrice,
46
50
  selectedBillingPeriod: billingPeriod,
47
51
  selectedTierByFeature: selectedTierByFeature || {},
52
+ perUnitQuantityByFeature: perUnitQuantityByFeature || {},
48
53
  };
49
54
 
50
55
  return paywallLocale.price.paid