@stigg/react-sdk 4.4.0-beta.3 β†’ 4.4.0-beta.5

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 (59) 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/usePreviewSubscription.d.ts +3 -0
  7. package/dist/components/checkout/hooks/useProgressBarModel.d.ts +2 -0
  8. package/dist/components/checkout/hooks/useSubscriptionModel.d.ts +2 -1
  9. package/dist/components/checkout/index.d.ts +1 -0
  10. package/dist/components/checkout/steps/payment/PaymentMethods.d.ts +3 -2
  11. package/dist/components/checkout/steps/payment/PaymentStep.d.ts +2 -1
  12. package/dist/components/checkout/steps/payment/stripe/StripePaymentForm.d.ts +2 -1
  13. package/dist/components/checkout/steps/payment/stripe/stripe.utils.d.ts +4 -0
  14. package/dist/components/checkout/steps/payment/stripe/useSubmit.d.ts +2 -1
  15. package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +6 -1
  16. package/dist/components/checkout/summary/CheckoutSummary.d.ts +3 -1
  17. package/dist/components/checkout/summary/components/LineItems.d.ts +2 -2
  18. package/dist/components/checkout/textOverrides.d.ts +4 -1
  19. package/dist/components/checkout/types.d.ts +7 -0
  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 +607 -271
  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 +619 -271
  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 +48 -22
  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 +34 -4
  37. package/src/components/checkout/hooks/useProgressBarModel.ts +15 -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 +41 -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/dist/components/checkout/steps/surprise/SurpriseStep.d.ts +0 -2
  58. package/src/assets/nyancat.svg +0 -634
  59. package/src/components/checkout/steps/surprise/SurpriseStep.tsx +0 -27
@@ -4,6 +4,7 @@ import { Alert, Grid } from '@mui/material';
4
4
  import { useCheckoutModel, usePaymentStepModel } from '../../hooks';
5
5
  import { ExistingPaymentMethod, NewPaymentMethod } from './PaymentMethods';
6
6
  import { Typography } from '../../../common/Typography';
7
+ import { CheckoutContainerProps } from '../../CheckoutContainer';
7
8
 
8
9
  const PaymentContainer = styled(Grid)`
9
10
  display: flex;
@@ -12,7 +13,7 @@ const PaymentContainer = styled(Grid)`
12
13
  margin: 32px 0;
13
14
  `;
14
15
 
15
- export function PaymentStep() {
16
+ export function PaymentStep({ onBillingAddressChange }: Pick<CheckoutContainerProps, 'onBillingAddressChange'>) {
16
17
  const { checkoutState, checkoutLocalization, widgetState } = useCheckoutModel();
17
18
  const { customer } = checkoutState || {};
18
19
  const { errorMessage, useNewPaymentMethod, setUseNewPaymentMethod } = usePaymentStepModel();
@@ -44,6 +45,7 @@ export function PaymentStep() {
44
45
  checked={useNewPaymentMethod}
45
46
  checkoutLocalization={checkoutLocalization}
46
47
  onSelect={() => handleOnSelect(true)}
48
+ onBillingAddressChange={onBillingAddressChange}
47
49
  />
48
50
  </PaymentContainer>
49
51
  );
@@ -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';
@@ -33,12 +33,16 @@ const StyledPlanCharge = styled.div`
33
33
 
34
34
  export function PlanCharge({
35
35
  charge,
36
+ isValid,
36
37
  setBillableFeature,
37
38
  billableFeature,
39
+ onValidationChange,
38
40
  }: {
39
41
  charge: Price;
42
+ isValid: boolean;
40
43
  billableFeature?: BillableFeatureInput;
41
44
  setBillableFeature: UsePlanStepModel['setBillableFeature'];
45
+ onValidationChange: ({ featureId, isValid }: { featureId: string; isValid: boolean }) => void;
42
46
  }) {
43
47
  const featureId = charge.feature?.featureId;
44
48
  const isBaseCharge = !featureId;
@@ -49,9 +53,17 @@ export function PlanCharge({
49
53
  const handleQuantityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
50
54
  if (isBaseCharge || !featureId) return;
51
55
 
52
- const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity || 1;
53
- const quantity = getValidPriceQuantity(charge, value || 1);
56
+ const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity;
57
+ if (!value || value <= 0) {
58
+ onValidationChange({ featureId, isValid: false });
59
+ // Reset the input value to null
60
+ // @ts-ignore
61
+ setBillableFeature(featureId, null);
62
+ return;
63
+ }
54
64
 
65
+ onValidationChange({ featureId, isValid: true });
66
+ const quantity = getValidPriceQuantity(charge, value);
55
67
  setBillableFeature(featureId, quantity);
56
68
  };
57
69
 
@@ -77,20 +89,23 @@ export function PlanCharge({
77
89
  tierUnits={getPriceFeatureUnit(charge)}
78
90
  selectedTier={tier}
79
91
  handleTierChange={(tier: PriceTierFragment) => {
80
- setBillableFeature(featureId!, tier!.upTo);
92
+ setBillableFeature(featureId!, tier.upTo);
81
93
  }}
82
94
  />
83
95
  );
84
96
  } else {
85
97
  chargeRow = (
86
98
  <InputField
87
- sx={{ width: 120 }}
99
+ sx={{ width: 145 }}
88
100
  id={`${featureId}-input`}
89
101
  type="number"
90
102
  InputProps={
91
103
  hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
92
104
  }
93
- value={billableFeature?.quantity || charge.minUnitQuantity || 1}
105
+ error={!isValid}
106
+ helperText={!isValid ? 'Not a valid value' : ''}
107
+ FormHelperTextProps={{ sx: { margin: '4px' } }}
108
+ value={billableFeature?.quantity || charge.minUnitQuantity || ''}
94
109
  onChange={handleQuantityChange}
95
110
  />
96
111
  );
@@ -118,18 +133,34 @@ export function PlanCharge({
118
133
 
119
134
  export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListProps) {
120
135
  const { billableFeatures, setBillableFeature } = usePlanStepModel();
121
- const planCharges = useChargesSort(plan?.pricePoints?.filter(p => p.billingPeriod === billingPeriod) || []);
136
+ const { setIsDisabled } = useProgressBarModel();
137
+ const planCharges = useChargesSort(plan?.pricePoints?.filter((p) => p.billingPeriod === billingPeriod) || []);
138
+ const [chargesValidation, setChargesValidation] = useState(
139
+ planCharges?.reduce<Record<string, boolean>>((acc, curr) => {
140
+ acc[curr.feature?.featureId || 'base-charge'] = true;
141
+ return acc;
142
+ }, {}),
143
+ );
144
+
145
+ useEffect(() => {
146
+ const isDisabled = Object.values(chargesValidation).some((x) => !x);
147
+ setIsDisabled(isDisabled);
148
+ }, [chargesValidation, setIsDisabled]);
122
149
 
123
150
  return (
124
151
  <div>
125
- {planCharges?.map(charge => {
126
- const billableFeature = billableFeatures.find(x => x.featureId === charge.feature?.featureId);
152
+ {planCharges?.map((charge) => {
153
+ const billableFeature = billableFeatures.find((x) => x.featureId === charge.feature?.featureId);
127
154
  return (
128
155
  <PlanCharge
129
156
  key={charge.feature?.featureId || 'base-charge'}
130
157
  charge={charge}
131
158
  setBillableFeature={setBillableFeature}
132
159
  billableFeature={billableFeature}
160
+ isValid={chargesValidation[charge.feature?.featureId || 'base-charge']}
161
+ onValidationChange={({ featureId, isValid }: { featureId: string; isValid: boolean }) =>
162
+ setChargesValidation((prev) => ({ ...prev, [featureId]: isValid }))
163
+ }
133
164
  />
134
165
  );
135
166
  })}
@@ -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
  }),