@stigg/react-sdk 4.4.0-beta.2 β†’ 4.4.0-beta.4

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 (60) hide show
  1. package/dist/components/checkout/Checkout.d.ts +1 -1
  2. package/dist/components/checkout/CheckoutContainer.d.ts +6 -2
  3. package/dist/components/checkout/CheckoutProvider.d.ts +3 -1
  4. package/dist/components/checkout/components/DowngradeToFreeContainer.d.ts +28 -0
  5. package/dist/components/checkout/hooks/usePaymentStepModel.d.ts +8 -2
  6. package/dist/components/checkout/hooks/useProgressBarModel.d.ts +3 -0
  7. package/dist/components/checkout/hooks/useSubscriptionModel.d.ts +2 -1
  8. package/dist/components/checkout/index.d.ts +1 -0
  9. package/dist/components/checkout/steps/payment/PaymentMethods.d.ts +3 -2
  10. package/dist/components/checkout/steps/payment/PaymentStep.d.ts +2 -1
  11. package/dist/components/checkout/steps/payment/stripe/StripePaymentForm.d.ts +2 -1
  12. package/dist/components/checkout/steps/payment/stripe/stripe.utils.d.ts +4 -0
  13. package/dist/components/checkout/steps/payment/stripe/useSubmit.d.ts +2 -1
  14. package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +5 -1
  15. package/dist/components/checkout/summary/CheckoutSummary.d.ts +3 -1
  16. package/dist/components/checkout/summary/components/LineItems.d.ts +2 -2
  17. package/dist/components/checkout/textOverrides.d.ts +4 -1
  18. package/dist/components/checkout/types.d.ts +7 -0
  19. package/dist/components/customerPortal/subscriptionOverview/subscriptionView/SubscriptionView.style.d.ts +1 -1
  20. package/dist/components/paywall/paywallTextOverrides.d.ts +4 -0
  21. package/dist/components/utils/getPaidPriceText.d.ts +3 -1
  22. package/dist/react-sdk.cjs.development.js +583 -263
  23. package/dist/react-sdk.cjs.development.js.map +1 -1
  24. package/dist/react-sdk.cjs.production.min.js +1 -1
  25. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  26. package/dist/react-sdk.esm.js +595 -263
  27. package/dist/react-sdk.esm.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/assets/payment-method.svg +3 -10
  30. package/src/components/checkout/Checkout.tsx +3 -1
  31. package/src/components/checkout/CheckoutContainer.tsx +54 -28
  32. package/src/components/checkout/CheckoutProvider.tsx +8 -4
  33. package/src/components/checkout/components/DowngradeToFreeContainer.tsx +98 -0
  34. package/src/components/checkout/hooks/usePaymentStepModel.ts +22 -3
  35. package/src/components/checkout/hooks/usePlanStepModel.ts +5 -5
  36. package/src/components/checkout/hooks/usePreviewSubscription.ts +26 -3
  37. package/src/components/checkout/hooks/useProgressBarModel.ts +18 -0
  38. package/src/components/checkout/hooks/useSubscriptionModel.ts +8 -2
  39. package/src/components/checkout/hooks/useSubscriptionState.ts +2 -1
  40. package/src/components/checkout/index.ts +1 -0
  41. package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +3 -2
  42. package/src/components/checkout/steps/payment/PaymentMethods.tsx +13 -6
  43. package/src/components/checkout/steps/payment/PaymentStep.tsx +3 -1
  44. package/src/components/checkout/steps/payment/stripe/StripePaymentForm.tsx +35 -4
  45. package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +4 -3
  46. package/src/components/checkout/steps/payment/stripe/useSubmit.ts +54 -45
  47. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +42 -10
  48. package/src/components/checkout/summary/CheckoutSuccess.tsx +1 -1
  49. package/src/components/checkout/summary/CheckoutSummary.tsx +24 -19
  50. package/src/components/checkout/summary/components/LineItems.tsx +8 -16
  51. package/src/components/checkout/textOverrides.ts +5 -4
  52. package/src/components/checkout/types.ts +9 -0
  53. package/src/components/paywall/PlanPrice.tsx +10 -2
  54. package/src/components/paywall/paywallTextOverrides.ts +3 -0
  55. package/src/components/utils/getPaidPriceText.ts +8 -2
  56. package/src/components/utils/getPlanPrice.ts +1 -1
  57. package/src/stories/Checkout.stories.tsx +4 -5
  58. package/dist/components/checkout/steps/surprise/SurpriseStep.d.ts +0 -2
  59. package/src/assets/nyancat.svg +0 -634
  60. package/src/components/checkout/steps/surprise/SurpriseStep.tsx +0 -27
@@ -1,22 +1,53 @@
1
1
  import React from 'react';
2
+ import { StripeAddressElementChangeEvent } from '@stripe/stripe-js';
2
3
  import { Grid } from '@mui/material';
4
+ import { BillingAddress } from '@stigg/js-client-sdk';
3
5
  import { AddressElement, PaymentElement } from '@stripe/react-stripe-js';
4
6
  import { Typography } from '../../../../common/Typography';
5
- import { useCheckoutModel } from '../../../hooks';
7
+ import { useCheckoutModel, usePaymentStepModel } from '../../../hooks';
8
+ import { CheckoutContainerProps } from '../../../CheckoutContainer';
6
9
 
7
- export function StripePaymentForm() {
8
- const { checkoutState, checkoutLocalization, widgetState } = useCheckoutModel();
10
+ export function StripePaymentForm({ onBillingAddressChange }: Pick<CheckoutContainerProps, 'onBillingAddressChange'>) {
11
+ const { checkoutState, checkoutLocalization, widgetState, setWidgetReadOnly } = useCheckoutModel();
12
+ const { setBillingAddress } = usePaymentStepModel();
9
13
  const { customer, configuration } = checkoutState || {};
10
14
  const { readOnly } = widgetState;
11
15
 
16
+ const handleAddressChange = (args: StripeAddressElementChangeEvent) => {
17
+ if (!args.complete) {
18
+ return;
19
+ }
20
+
21
+ const { postal_code: postalCode, ...addressFields } = args.value.address;
22
+ const billingAddress: BillingAddress = {
23
+ postalCode,
24
+ ...addressFields,
25
+ };
26
+
27
+ setWidgetReadOnly(true);
28
+ setBillingAddress(billingAddress);
29
+
30
+ if (onBillingAddressChange) {
31
+ const callExternalBillingAddressChanged = async () => {
32
+ await onBillingAddressChange({ billingAddress });
33
+ setWidgetReadOnly(false);
34
+ };
35
+
36
+ void callExternalBillingAddressChanged();
37
+ } else {
38
+ setWidgetReadOnly(false);
39
+ }
40
+ };
41
+
12
42
  return (
13
43
  <Grid flexDirection="column" container gap={3} padding="16px" sx={{ pointerEvents: readOnly ? 'none' : undefined }}>
14
44
  <Grid flexDirection="column" container gap={2}>
15
45
  <Typography variant="h6">{checkoutLocalization.newPaymentMethodBillingAddressTitle}</Typography>
16
46
  <AddressElement
47
+ onChange={handleAddressChange}
17
48
  options={{
18
49
  mode: 'billing',
19
- fields: { phone: !!configuration?.content?.collectPhoneNumber ? 'always' : 'auto' },
50
+ fields: { phone: configuration?.content?.collectPhoneNumber ? 'always' : 'auto' },
20
51
  defaultValues: {
21
52
  ...(customer?.name && { name: customer.name }),
22
53
  },
@@ -8,15 +8,16 @@ type StripeElementsProps = {
8
8
 
9
9
  export async function handleStripeFormValidations({ elements }: Pick<StripeElementsProps, 'elements'>) {
10
10
  if (!elements) {
11
- console.error('Stripe elements not initialized');
12
- return { success: false };
11
+ const errorMessage = 'Stripe elements not initialized';
12
+ console.error(errorMessage);
13
+ return { success: false, errorMessage };
13
14
  }
14
15
 
15
16
  const { error: elementsError } = await elements.submit();
16
17
 
17
18
  if (elementsError) {
18
19
  console.log(elementsError.message);
19
- return { success: false };
20
+ return { success: false, errorMessage: elementsError.message || '' };
20
21
  }
21
22
 
22
23
  return { success: true };
@@ -8,9 +8,9 @@ import { CheckoutContainerProps, CheckoutResult } from '../../../CheckoutContain
8
8
  import { handleNewPaymentMethod, handleStripeFormValidations, handleStripeNextAction } from './stripe.utils';
9
9
  import { ANIMATION_DURATION } from '../../../summary/CheckoutSuccess';
10
10
 
11
- const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
11
+ const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
12
12
 
13
- export type HandleSubmitResult = { results?: ApplySubscriptionResults; errorMessage?: string } | undefined;
13
+ export type HandleSubmitResult = { results?: ApplySubscriptionResults; success: boolean; errorMessage?: string };
14
14
 
15
15
  export type UseSubmitProps = {
16
16
  onSuccess?: () => void;
@@ -29,61 +29,69 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess }: UseSub
29
29
  e.preventDefault();
30
30
 
31
31
  if (!subscriptionState) {
32
- return;
32
+ return { success: false, errorMessage: 'Unexpected error, please contact support.' };
33
33
  }
34
34
 
35
+ let checkoutParams: ApplySubscription = { ...subscriptionState };
35
36
  let checkoutResults: ApplySubscriptionResults | undefined;
36
37
  let errorMessage: string | undefined;
37
38
  let paymentMethodId: string | undefined;
38
39
 
39
- setWidgetReadOnly(true);
40
+ const checkoutAction = async (): Promise<CheckoutResult> => {
41
+ if (useNewPaymentMethod) {
42
+ const { success, errorMessage: stripeValidationsErrorMessage } = await handleStripeFormValidations({
43
+ elements,
44
+ });
45
+
46
+ if (!success) {
47
+ return { success: false, errorMessage: stripeValidationsErrorMessage };
48
+ }
40
49
 
41
- if (useNewPaymentMethod) {
42
- const { success } = await handleStripeFormValidations({ elements });
43
- if (!success) {
44
- setWidgetReadOnly(false);
45
- return;
50
+ const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
51
+ if (!paymentMethodResults.success) {
52
+ errorMessage = paymentMethodResults.errorMessage;
53
+ }
54
+
55
+ paymentMethodId = paymentMethodResults.paymentMethodId;
46
56
  }
47
57
 
48
- const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
49
- if (!paymentMethodResults.success) {
50
- errorMessage = paymentMethodResults.errorMessage;
58
+ if (errorMessage) {
59
+ return { success: false, errorMessage };
51
60
  }
52
61
 
53
- paymentMethodId = paymentMethodResults.paymentMethodId;
54
- }
62
+ checkoutParams = { ...checkoutParams, paymentMethodId };
55
63
 
56
- if (!errorMessage) {
57
- const checkoutParams: ApplySubscription = { ...subscriptionState, paymentMethodId };
58
-
59
- const checkoutAction = async (): Promise<CheckoutResult> => {
60
- try {
61
- const applySubscriptionResults = await stigg.applySubscription(checkoutParams);
62
- const nextActionResults = await handleStripeNextAction({
63
- applySubscriptionResults,
64
- stripe,
65
- });
66
-
67
- checkoutResults = nextActionResults;
68
- if (nextActionResults.errorMessage) {
69
- errorMessage = nextActionResults.errorMessage;
70
- }
71
-
72
- return { success: !nextActionResults.errorMessage, errorMessage: nextActionResults.errorMessage };
73
- } catch (e) {
74
- console.error(e);
75
- errorMessage = (e as any)?.message;
76
- return { success: false, errorMessage };
77
- }
78
- };
64
+ try {
65
+ const applySubscriptionResults = await stigg.applySubscription(checkoutParams);
66
+ const nextActionResults = await handleStripeNextAction({
67
+ applySubscriptionResults,
68
+ stripe,
69
+ });
79
70
 
80
- if (onCheckout) {
81
- const externalCheckoutResults = await onCheckout({ checkoutParams, checkoutAction });
82
- if (!externalCheckoutResults.success && externalCheckoutResults.errorMessage) {
83
- errorMessage = externalCheckoutResults.errorMessage;
71
+ checkoutResults = nextActionResults;
72
+ if (nextActionResults.errorMessage) {
73
+ errorMessage = nextActionResults.errorMessage;
84
74
  }
85
- } else {
86
- await checkoutAction();
75
+
76
+ return { success: !nextActionResults.errorMessage, errorMessage: nextActionResults.errorMessage };
77
+ } catch (e) {
78
+ console.error(e);
79
+ errorMessage = (e as any)?.message;
80
+ return { success: false, errorMessage };
81
+ }
82
+ };
83
+
84
+ setWidgetReadOnly(true);
85
+
86
+ if (onCheckout) {
87
+ const externalCheckoutResults = await onCheckout({ checkoutParams, checkoutAction });
88
+ if (!externalCheckoutResults.success && externalCheckoutResults.errorMessage) {
89
+ errorMessage = externalCheckoutResults.errorMessage;
90
+ }
91
+ } else {
92
+ const checkoutActionResults = await checkoutAction();
93
+ if (!checkoutActionResults.success && checkoutActionResults.errorMessage) {
94
+ errorMessage = checkoutActionResults.errorMessage;
87
95
  }
88
96
  }
89
97
 
@@ -92,12 +100,13 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess }: UseSub
92
100
  const success = !errorMessage && !!checkoutResults?.subscription;
93
101
  if (success && onSuccess) {
94
102
  onSuccess();
103
+
104
+ await delay(ANIMATION_DURATION); // Wait for animation to finish
95
105
  }
96
106
 
97
- await delay(ANIMATION_DURATION); // Wait for animation to finish
98
107
  await onCheckoutCompleted({ success, error: errorMessage });
99
108
 
100
- return { results: checkoutResults, errorMessage };
109
+ return { results: checkoutResults, success: !errorMessage, errorMessage };
101
110
  };
102
111
 
103
112
  return { handleSubmit, isLoading: !!widgetState?.readOnly };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
 
3
3
  import styled from '@emotion/styled';
4
4
  import { Box } from '@mui/material';
@@ -10,7 +10,7 @@ import { useChargesSort } from '../../../hooks/useChargeSort';
10
10
  import { calculateUnitQuantityText } from '../../../paywall/utils/calculateUnitQuantityText';
11
11
  import { currencyPriceFormatter } from '../../../utils/currencyUtils';
12
12
  import { InputField } from '../../components';
13
- import { usePlanStepModel } from '../../hooks';
13
+ import { usePlanStepModel, useProgressBarModel } from '../../hooks';
14
14
  import { TiersSelectContainer } from '../../../common/TiersSelectContainer';
15
15
  import { getPriceFeatureUnit, getTierByQuantity } from '../../../utils/priceTierUtils';
16
16
  import { getValidPriceQuantity } from '../../../utils/priceUtils';
@@ -35,23 +35,42 @@ export function PlanCharge({
35
35
  charge,
36
36
  setBillableFeature,
37
37
  billableFeature,
38
+ onValidationChange,
38
39
  }: {
39
40
  charge: Price;
40
41
  billableFeature?: BillableFeatureInput;
41
42
  setBillableFeature: UsePlanStepModel['setBillableFeature'];
43
+ onValidationChange: ({ featureId, isValid }: { featureId: string; isValid: boolean }) => void;
42
44
  }) {
45
+ const [isValid, setIsValid] = React.useState(true);
43
46
  const featureId = charge.feature?.featureId;
44
47
  const isBaseCharge = !featureId;
45
48
  const isPayAsYouGo = charge.pricingModel === BillingModel.UsageBased;
46
49
  const displayName = isBaseCharge ? 'Base charge' : charge.feature?.displayName;
47
50
  const hasQuantityRestrictions = !!(charge?.minUnitQuantity || charge?.maxUnitQuantity);
48
51
 
52
+ useEffect(() => {
53
+ if (!featureId) {
54
+ return;
55
+ }
56
+
57
+ onValidationChange({ featureId, isValid });
58
+ }, [featureId, isValid, onValidationChange]);
59
+
49
60
  const handleQuantityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
50
61
  if (isBaseCharge || !featureId) return;
51
62
 
52
- const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity || 1;
53
- const quantity = getValidPriceQuantity(charge, value || 1);
63
+ const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity;
64
+ if (!value || value <= 0) {
65
+ setIsValid(false);
66
+ // Reset the input value to null
67
+ // @ts-ignore
68
+ setBillableFeature(featureId, null);
69
+ return;
70
+ }
54
71
 
72
+ setIsValid(true);
73
+ const quantity = getValidPriceQuantity(charge, value);
55
74
  setBillableFeature(featureId, quantity);
56
75
  };
57
76
 
@@ -77,20 +96,23 @@ export function PlanCharge({
77
96
  tierUnits={getPriceFeatureUnit(charge)}
78
97
  selectedTier={tier}
79
98
  handleTierChange={(tier: PriceTierFragment) => {
80
- setBillableFeature(featureId!, tier!.upTo);
99
+ setBillableFeature(featureId!, tier.upTo);
81
100
  }}
82
101
  />
83
102
  );
84
103
  } else {
85
104
  chargeRow = (
86
105
  <InputField
87
- sx={{ width: 120 }}
106
+ sx={{ width: 145 }}
88
107
  id={`${featureId}-input`}
89
108
  type="number"
90
109
  InputProps={
91
110
  hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
92
111
  }
93
- value={billableFeature?.quantity || charge.minUnitQuantity || 1}
112
+ error={!isValid}
113
+ helperText={!isValid ? 'Not a valid value' : ''}
114
+ FormHelperTextProps={{ sx: { margin: '4px' } }}
115
+ value={billableFeature?.quantity || charge.minUnitQuantity}
94
116
  onChange={handleQuantityChange}
95
117
  />
96
118
  );
@@ -118,18 +140,28 @@ export function PlanCharge({
118
140
 
119
141
  export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListProps) {
120
142
  const { billableFeatures, setBillableFeature } = usePlanStepModel();
121
- const planCharges = useChargesSort(plan?.pricePoints?.filter(p => p.billingPeriod === billingPeriod) || []);
143
+ const { setIsDisabled } = useProgressBarModel();
144
+ const planCharges = useChargesSort(plan?.pricePoints?.filter((p) => p.billingPeriod === billingPeriod) || []);
145
+ const [chargesValidation, setChargesValidation] = useState({});
146
+
147
+ useEffect(() => {
148
+ const isDisabled = Object.values(chargesValidation).some((x) => !x);
149
+ setIsDisabled(isDisabled);
150
+ }, [chargesValidation, setIsDisabled]);
122
151
 
123
152
  return (
124
153
  <div>
125
- {planCharges?.map(charge => {
126
- const billableFeature = billableFeatures.find(x => x.featureId === charge.feature?.featureId);
154
+ {planCharges?.map((charge) => {
155
+ const billableFeature = billableFeatures.find((x) => x.featureId === charge.feature?.featureId);
127
156
  return (
128
157
  <PlanCharge
129
158
  key={charge.feature?.featureId || 'base-charge'}
130
159
  charge={charge}
131
160
  setBillableFeature={setBillableFeature}
132
161
  billableFeature={billableFeature}
162
+ onValidationChange={({ featureId, isValid }: { featureId: string; isValid: boolean }) =>
163
+ setChargesValidation((prev) => ({ ...prev, [featureId]: isValid }))
164
+ }
133
165
  />
134
166
  );
135
167
  })}
@@ -24,7 +24,7 @@ const CheckoutSuccessContainer = styled(Box)`
24
24
  export function CheckoutSuccess() {
25
25
  return (
26
26
  <CheckoutSuccessContainer>
27
- <Lottie width={350} options={{ loop: false, autoplay: true, animationData }} />
27
+ <Lottie width={350} isClickToPauseDisabled options={{ loop: false, autoplay: true, animationData }} />
28
28
  </CheckoutSuccessContainer>
29
29
  );
30
30
  }
@@ -84,7 +84,12 @@ function resolveCheckoutButtonText({
84
84
  return checkoutLocalization.checkoutButton.purchaseText;
85
85
  }
86
86
 
87
- export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutContainerProps) => {
87
+ export const CheckoutSummary = ({
88
+ onCheckout,
89
+ onCheckoutCompleted,
90
+ disablePromotionCode,
91
+ isFreeDowngrade,
92
+ }: CheckoutContainerProps & { isFreeDowngrade: boolean }) => {
88
93
  const [isCheckoutCompletedSuccessfully, setIsCheckoutCompletedSuccessfully] = useState(false);
89
94
  const { setErrorMessage } = usePaymentStepModel();
90
95
  const progressBar = useProgressBarModel();
@@ -96,8 +101,7 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
96
101
  );
97
102
  const [baseCharges, usageCharges] = partition(planPrices, (price) => price.pricingModel === BillingModel.FlatFee);
98
103
  const [baseCharge] = baseCharges || [];
99
- const isFirstSubscription = !activeSubscription?.id;
100
- const isLastStep = progressBar.isCheckoutComplete && progressBar.isLastStep;
104
+ const isLastStep = isFreeDowngrade || (progressBar.isCheckoutComplete && progressBar.isLastStep);
101
105
 
102
106
  const { subscriptionPreview, isFetchingSubscriptionPreview } = usePreviewSubscription();
103
107
 
@@ -119,11 +123,9 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
119
123
  if (errorMessage) {
120
124
  setErrorMessage(errorMessage);
121
125
  setIsCheckoutCompletedSuccessfully(false);
122
- return;
126
+ } else {
127
+ setErrorMessage(undefined);
123
128
  }
124
-
125
- setErrorMessage(undefined);
126
- setIsCheckoutCompletedSuccessfully(true);
127
129
  };
128
130
 
129
131
  const onCheckoutClick = async (e: any): Promise<void> => {
@@ -211,7 +213,7 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
211
213
 
212
214
  <StyledDivider className="stigg-checkout-summary-divider" />
213
215
 
214
- {isFirstSubscription && (
216
+ {!disablePromotionCode && (
215
217
  <>
216
218
  <PromotionCodeSection checkoutLocalization={checkoutLocalization} />
217
219
 
@@ -219,18 +221,18 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
219
221
  </>
220
222
  )}
221
223
 
222
- <AppliedCreditsLineItem
224
+ <DiscountLineItem
223
225
  subscriptionPreview={subscriptionPreview}
224
226
  isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
225
- checkoutLocalization={checkoutLocalization}
226
227
  />
227
228
 
228
- <DiscountLineItem
229
+ <TaxLineItem
229
230
  subscriptionPreview={subscriptionPreview}
230
231
  isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
232
+ checkoutLocalization={checkoutLocalization}
231
233
  />
232
234
 
233
- <TaxLineItem
235
+ <AppliedCreditsLineItem
234
236
  subscriptionPreview={subscriptionPreview}
235
237
  isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
236
238
  checkoutLocalization={checkoutLocalization}
@@ -256,10 +258,11 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
256
258
  />
257
259
 
258
260
  <Button
259
- disableRipple={isLoading}
260
- $isLoading={isLoading}
261
+ disableRipple={isLoading || progressBar.progressBarState.isDisabled}
262
+ $isLoading={isLoading || progressBar.progressBarState.isDisabled}
261
263
  $success={isCheckoutCompletedSuccessfully}
262
264
  $error={isLastStep && subscriptionPreview?.isPlanDowngrade}
265
+ disabled={isLoading || isFetchingSubscriptionPreview}
263
266
  className="stigg-checkout-summary-cta-button"
264
267
  sx={{ textTransform: 'none', borderRadius: '10px', marginTop: '24px' }}
265
268
  variant="contained"
@@ -269,11 +272,13 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
269
272
  }}
270
273
  fullWidth>
271
274
  <Typography className="stigg-checkout-summary-cta-button-text" color="white" style={{ display: 'flex' }}>
272
- {isCheckoutCompletedSuccessfully && <Icon icon="Check" style={{ display: 'contents' }} />}
273
- {isLoading && <CircularProgress size={20} sx={{ color: 'white' }} />}
274
- {!isLoading &&
275
- !isCheckoutCompletedSuccessfully &&
276
- resolveCheckoutButtonText({ isLastStep, subscriptionPreview, checkoutLocalization })}
275
+ {isCheckoutCompletedSuccessfully ? (
276
+ <Icon icon="Check" style={{ display: 'contents' }} />
277
+ ) : isLoading || isFetchingSubscriptionPreview ? (
278
+ <CircularProgress size={20} sx={{ color: 'white' }} />
279
+ ) : (
280
+ resolveCheckoutButtonText({ isLastStep, subscriptionPreview, checkoutLocalization })
281
+ )}
277
282
  </Typography>
278
283
  </Button>
279
284
  </SummaryCard>
@@ -21,24 +21,16 @@ export const LineItemRow = styled.div`
21
21
  justify-content: space-between;
22
22
  `;
23
23
 
24
- export const getPriceString = ({
25
- amountPerMonth,
26
- price,
27
- quantity,
28
- }: {
29
- amountPerMonth: number;
30
- price: Price;
31
- quantity: number;
32
- }) => {
24
+ export const getPriceString = ({ amount, price, quantity }: { amount: number; price: Price; quantity: number }) => {
33
25
  const { billingPeriod } = price;
34
26
  let billingPeriodString = null;
35
27
 
36
28
  if (billingPeriod === BillingPeriod.Annually) {
37
- amountPerMonth /= 12;
29
+ amount /= 12;
38
30
  billingPeriodString = '12 months';
39
31
  }
40
32
 
41
- const addonPriceFormat = currencyPriceFormatter({ amount: amountPerMonth, currency: price.currency });
33
+ const addonPriceFormat = currencyPriceFormatter({ amount, currency: price.currency });
42
34
 
43
35
  return `${quantity > 1 ? `${quantity} x ${addonPriceFormat} each` : addonPriceFormat}${
44
36
  billingPeriodString ? ` x ${billingPeriodString}` : ''
@@ -48,12 +40,12 @@ export const getPriceString = ({
48
40
  export const BilledPriceLineItem = ({ label, quantity, price }: { label: string; quantity: number; price: Price }) => {
49
41
  const { billingPeriod } = price;
50
42
 
51
- let amountPerMonth;
43
+ let amount;
52
44
  if (price.isTieredPrice) {
53
45
  const tier = getTierByQuantity(price.tiers!, quantity);
54
- amountPerMonth = tier!.unitPrice.amount!;
46
+ amount = tier!.unitPrice.amount!;
55
47
  } else {
56
- amountPerMonth = price.amount!;
48
+ amount = price.amount!;
57
49
  }
58
50
 
59
51
  return (
@@ -65,13 +57,13 @@ export const BilledPriceLineItem = ({ label, quantity, price }: { label: string;
65
57
  </Typography>
66
58
  {(quantity > 1 || billingPeriod === BillingPeriod.Annually) && (
67
59
  <Typography variant="body1" color="secondary">
68
- {getPriceString({ amountPerMonth, price, quantity })}
60
+ {getPriceString({ amount, price, quantity })}
69
61
  </Typography>
70
62
  )}
71
63
  </Grid>
72
64
  <Grid item>
73
65
  <Typography variant="body1" color="secondary" style={{ wordBreak: 'break-word' }}>
74
- {currencyPriceFormatter({ amount: quantity * amountPerMonth, currency: price.currency })}
66
+ {currencyPriceFormatter({ amount: quantity * amount, currency: price.currency })}
75
67
  </Typography>
76
68
  </Grid>
77
69
  </LineItemRow>
@@ -1,8 +1,7 @@
1
- import { BillingPeriod, SubscriptionPreviewTaxDetails } from '@stigg/js-client-sdk';
1
+ import { BillingPeriod, Plan, SubscriptionPreviewTaxDetails } from '@stigg/js-client-sdk';
2
2
  import moment from 'moment';
3
3
  import merge from 'lodash/merge';
4
4
  import { DeepPartial } from '../../types';
5
- import { formatBillingPeriod } from './formatting';
6
5
 
7
6
  export type CheckoutLocalization = {
8
7
  changePlan: string;
@@ -26,6 +25,7 @@ export type CheckoutLocalization = {
26
25
  };
27
26
  appliedCreditsTitle: string;
28
27
  taxTitle: (params: { taxDetails: SubscriptionPreviewTaxDetails }) => string;
28
+ downgradeToFreeAlertText: (params: { plan: Plan }) => string;
29
29
  };
30
30
 
31
31
  export function getResolvedCheckoutLocalize(
@@ -38,7 +38,7 @@ export function getResolvedCheckoutLocalize(
38
38
  newPaymentMethodText: 'New payment method',
39
39
  newPaymentMethodBillingAddressTitle: 'Billing address',
40
40
  newPaymentMethodCardTitle: 'Payment method',
41
- baseChargeText: ({ billingPeriod }) => `${formatBillingPeriod(billingPeriod)} charge`,
41
+ baseChargeText: () => 'Base charge',
42
42
  totalText: 'Total due today',
43
43
  subTotalText: 'Subtotal',
44
44
  addCouponCodeText: 'Add coupon code',
@@ -55,7 +55,8 @@ export function getResolvedCheckoutLocalize(
55
55
  purchaseText: 'Purchase',
56
56
  },
57
57
  appliedCreditsTitle: 'Applied credits',
58
- taxTitle: ({ taxDetails }) => `${taxDetails.displayName} (${taxDetails?.percentage}%)`,
58
+ taxTitle: ({ taxDetails }) => `Tax (${taxDetails?.percentage}%)`,
59
+ downgradeToFreeAlertText: () => `We’re sorry to see you downgrade your plan 😞`,
59
60
  };
60
61
 
61
62
  return merge(checkoutDefaultLocalization, localizeOverride);
@@ -0,0 +1,9 @@
1
+ import { SubscriptionBillingInfo } from '@stigg/js-client-sdk';
2
+
3
+ export type BillingInformation = {
4
+ taxDetails?: TaxDetailsInput;
5
+ };
6
+
7
+ export type TaxDetailsInput = {
8
+ taxPercentage?: SubscriptionBillingInfo['taxPercentage'];
9
+ };
@@ -28,14 +28,21 @@ type PriceBillingPeriodProps = {
28
28
  billingPeriod: BillingPeriod;
29
29
  hasMonthlyPrice: boolean;
30
30
  hasAnnuallyPrice: boolean;
31
+ paywallLocale: PaywallLocalization;
31
32
  };
32
33
 
33
- function PriceBillingPeriod({ plan, billingPeriod, hasMonthlyPrice, hasAnnuallyPrice }: PriceBillingPeriodProps) {
34
+ function PriceBillingPeriod({
35
+ plan,
36
+ billingPeriod,
37
+ hasMonthlyPrice,
38
+ hasAnnuallyPrice,
39
+ paywallLocale,
40
+ }: PriceBillingPeriodProps) {
34
41
  const hasPrice = plan.pricePoints.find(pricePoint => pricePoint.billingPeriod === billingPeriod);
35
42
 
36
43
  let content = EMPTY_CHAR;
37
44
  if (hasPrice && hasMonthlyPrice && hasAnnuallyPrice) {
38
- content = `, billed ${billingPeriod.toLowerCase()}`;
45
+ content = paywallLocale.price.billingPeriod?.(billingPeriod) || `, billed ${billingPeriod.toLowerCase()}`;
39
46
  }
40
47
 
41
48
  return (
@@ -133,6 +140,7 @@ export const PlanPrice = ({
133
140
  billingPeriod={billingPeriod}
134
141
  hasAnnuallyPrice={hasAnnuallyPrice}
135
142
  hasMonthlyPrice={hasMonthlyPrice}
143
+ paywallLocale={paywallLocale}
136
144
  />
137
145
  </Typography>
138
146
  )}
@@ -26,6 +26,8 @@ export type PaywallLocalization = {
26
26
  };
27
27
  price: {
28
28
  startingAtCaption: string;
29
+ billingPeriod?: (billingPeriod: BillingPeriod) => string;
30
+ pricePeriod: (billingPeriod: BillingPeriod) => string;
29
31
  custom: string;
30
32
  priceNotSet: string;
31
33
  free: PlanPriceText | ((currency?: PaywallCurrency) => PlanPriceText);
@@ -56,6 +58,7 @@ export function getResolvedPaywallLocalize(localizeOverride?: DeepPartial<Paywal
56
58
  },
57
59
  price: {
58
60
  startingAtCaption: 'Starts at',
61
+ pricePeriod: (billingPeriod: BillingPeriod) => (billingPeriod === BillingPeriod.Monthly ? '/ month' : '/ year'),
59
62
  free: currency => ({
60
63
  price: `${currency?.symbol}0`,
61
64
  }),
@@ -8,6 +8,7 @@ import {
8
8
  import { currencyPriceFormatter } from './currencyUtils';
9
9
  import { PlanPriceText } from './getPlanPrice';
10
10
  import { calculateTierPrice, getPriceFeatureUnit } from './priceTierUtils';
11
+ import { PaywallLocalization } from '../paywall';
11
12
 
12
13
  type GetPaidPriceTextParams = {
13
14
  planPrices: Price[];
@@ -16,6 +17,7 @@ type GetPaidPriceTextParams = {
16
17
  locale: string;
17
18
  shouldShowMonthlyPriceAmount: boolean;
18
19
  selectedTierByFeature: Record<string, PriceTierFragment>;
20
+ paywallLocale: PaywallLocalization;
19
21
  };
20
22
 
21
23
  export function getPaidPriceText({
@@ -25,6 +27,7 @@ export function getPaidPriceText({
25
27
  locale,
26
28
  shouldShowMonthlyPriceAmount,
27
29
  selectedTierByFeature,
30
+ paywallLocale,
28
31
  }: GetPaidPriceTextParams): PlanPriceText {
29
32
  const { amount, currency } = paywallCalculatedPrice || planPrices[0];
30
33
  const priceAmount = amount || 0;
@@ -35,7 +38,10 @@ export function getPaidPriceText({
35
38
 
36
39
  let tiers;
37
40
  let tierUnits;
38
- let unit = shouldShowMonthlyPriceAmount ? '/ month' : '/ year';
41
+ const pricePeriod = paywallLocale.price.pricePeriod(
42
+ shouldShowMonthlyPriceAmount ? BillingPeriod.Monthly : BillingPeriod.Annually,
43
+ );
44
+ let unit = pricePeriod;
39
45
 
40
46
  for (const price of planPrices) {
41
47
  if (price.isTieredPrice) {
@@ -56,7 +62,7 @@ export function getPaidPriceText({
56
62
  const featureUnit = price.feature?.units || '';
57
63
 
58
64
  if (price.pricingModel === BillingModel.PerUnit && !price.isTieredPrice) {
59
- unit = shouldShowMonthlyPriceAmount ? `per ${featureUnit} / month` : `per ${featureUnit} / year`;
65
+ unit = shouldShowMonthlyPriceAmount ? `per ${featureUnit} ${pricePeriod}` : `per ${featureUnit} ${pricePeriod}`;
60
66
  } else if (price.pricingModel === BillingModel.UsageBased) {
61
67
  unit = `per ${featureUnit}`;
62
68
  }
@@ -50,7 +50,7 @@ export function getPlanPrice(
50
50
 
51
51
  return paywallLocale.price.paid
52
52
  ? paywallLocale.price.paid({ ...paidParams, plan })
53
- : getPaidPriceText({ ...paidParams, locale, shouldShowMonthlyPriceAmount });
53
+ : getPaidPriceText({ ...paidParams, locale, shouldShowMonthlyPriceAmount, paywallLocale });
54
54
  }
55
55
  default:
56
56
  return {