Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
mrazadar/PON-208/resubscribe-subscription (#744)
Browse files Browse the repository at this point in the history
* feat: handle resubscribe state

* feat: stripe-checkout post increased timeout to 15 seconds

* refactor: improved error handling to fallback-error

* feat: removed fractional zeros with price if fraction is zero

* test: fixed the i18n warnings and some tests

* feat: added the resubscribe states

* refactor: fix the lint issue

* feat: imprved the PR feedback

* feat: improved error handling

* refactor: the empty cart message handling

* refactor: lowrer case the ENABLE_B2C_SUBSCRIPTIONS flag before checking

* refactor: empty cart message placement fix
  • Loading branch information
mrazadar authored May 18, 2023
1 parent 7174039 commit ecd4446
Show file tree
Hide file tree
Showing 25 changed files with 215 additions and 69 deletions.
12 changes: 7 additions & 5 deletions src/feedback/data/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ export function* handleSubscriptionErrors(e, clearExistingMessages) {
if (e.errors !== undefined) {
for (let i = 0; i < e.errors.length; i++) { // eslint-disable-line no-plusplus
const error = e.errors[i];
/**
* * If msg has errorCode and userMessage show it otherwise fallback-error
* */
if (error.code && error.userMessage) {
yield put(addMessage(error.code, error.userMessage, error?.data, error.messageType || MESSAGE_TYPES.ERROR));
const customErrors = [
'empty_subscription',
'embargo-error',
'basket-changed-error',
];
if (customErrors.includes(error.code)) {
yield put(addMessage(error.code, error.userMessage, error?.data, MESSAGE_TYPES.ERROR));
} else {
yield put(addMessage('fallback-error', error.userMessage, error?.data, MESSAGE_TYPES.ERROR));
}
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import frCAMessages from './messages/fr_CA.json';
import deDEMessages from './messages/de_DE.json';
import itITMessages from './messages/it_IT.json';
import ptPTMessages from './messages/pt_PT.json';
import faMessages from './messages/fa.json';
import faIRMessages from './messages/fa_IR.json';
// no need to import en messages-- they are in the defaultMessage field

const messages = {
Expand All @@ -31,6 +33,8 @@ const messages = {
'de-de': deDEMessages,
'it-it': itITMessages,
'pt-pt': ptPTMessages,
fa: faMessages,
'fa-ir': faIRMessages,
};

export default messages;
2 changes: 2 additions & 0 deletions src/i18n/messages/fa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 2 additions & 0 deletions src/i18n/messages/fa_IR.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
2 changes: 1 addition & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ subscribe(APP_READY, () => {
<Switch>
<Route exact path="/" component={PaymentPage} />
{
getConfig().ENABLE_B2C_SUBSCRIPTIONS === 'true' ? (
getConfig().ENABLE_B2C_SUBSCRIPTIONS?.toLowerCase() === 'true' ? (
<Route exact path="/subscription" component={SubscriptionPage} />
) : null
}
Expand Down
8 changes: 7 additions & 1 deletion src/payment/cart/LocalizedPrice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedNumber } from '@edx/frontend-platform/i18n';

import { localizedCurrencySelector } from '../data/utils';
import { localizedCurrencySelector, getPropsToRemoveFractionZeroDigits } from '../data/utils';

/**
* Displays a positive or negative price, according to the currency and the conversion rate set
Expand Down Expand Up @@ -38,6 +38,10 @@ const LocalizedPrice = (props) => {
value={price}
style="currency" // eslint-disable-line react/style-prop-object
currency={props.currencyCode}
{...getPropsToRemoveFractionZeroDigits({
price,
shouldRemoveFractionZeroDigits: props.shouldRemoveFractionZeroDigits,
})}
/>
);
};
Expand All @@ -50,13 +54,15 @@ LocalizedPrice.propTypes = {
conversionRate: PropTypes.number,
currencyCode: PropTypes.string,
showAsLocalizedCurrency: PropTypes.bool,
shouldRemoveFractionZeroDigits: PropTypes.bool,
};

LocalizedPrice.defaultProps = {
amount: undefined,
conversionRate: 1,
currencyCode: 'USD',
showAsLocalizedCurrency: false,
shouldRemoveFractionZeroDigits: false,
};

export default connect(localizedCurrencySelector)(LocalizedPrice);
13 changes: 1 addition & 12 deletions src/payment/cart/SummaryTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';

import LocalizedPrice from './LocalizedPrice';

const SummaryTable = ({ price, isSubscription }) => (
const SummaryTable = ({ price }) => (
<div className="summary-row d-flex">
<span className="flex-grow-1">
<FormattedMessage
Expand All @@ -15,26 +15,15 @@ const SummaryTable = ({ price, isSubscription }) => (
</span>
<span className="summary-price">
<LocalizedPrice amount={price} />
{
isSubscription ? (
<FormattedMessage
id="subscription.summary.table.label.price"
defaultMessage="/month USD after 7-day free trial"
description="Label for subscription on order summary table"
/>
) : null
}
</span>
</div>
);

SummaryTable.propTypes = {
price: PropTypes.number,
isSubscription: PropTypes.bool,
};
SummaryTable.defaultProps = {
price: undefined,
isSubscription: false,
};

export default SummaryTable;
6 changes: 4 additions & 2 deletions src/payment/cart/TotalTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';

import LocalizedPrice from './LocalizedPrice';

const TotalTable = ({ total, isSubscription }) => (
const TotalTable = ({ total, isSubscription, shouldRemoveFractionZeroDigits }) => (
<div className="summary-row font-weight-bold d-flex">
<span className="flex-grow-1">
{isSubscription ? (
Expand All @@ -22,18 +22,20 @@ const TotalTable = ({ total, isSubscription }) => (
)}
</span>
<span className="text-right">
<LocalizedPrice amount={total} />
<LocalizedPrice amount={total} shouldRemoveFractionZeroDigits={shouldRemoveFractionZeroDigits} />
</span>
</div>
);

TotalTable.propTypes = {
total: PropTypes.number,
isSubscription: PropTypes.bool,
shouldRemoveFractionZeroDigits: PropTypes.bool,
};
TotalTable.defaultProps = {
total: undefined,
isSubscription: false,
shouldRemoveFractionZeroDigits: false,
};

export default TotalTable;
22 changes: 22 additions & 0 deletions src/payment/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,25 @@ export const localizedCurrencySelector = () => {
showAsLocalizedCurrency,
};
};

/**
* getPropsToRemoveFractionZeroDigits()
* [problem] react-i18n FormattedNumber currency automatically appends .00 fraction zero digits
* this function will return i18n props for removing decimal zero fraction only if fraction value is zero
* and if shouldRemoveFractionZeroDigits boolean value is true
* params {price: number}
* params {shouldRemoveFractionZeroDigits: boolean}
*/
export const getPropsToRemoveFractionZeroDigits = ({ price, shouldRemoveFractionZeroDigits }) => {
let props = {};
if (shouldRemoveFractionZeroDigits) {
const fractionValue = price.toString().split('.')[1];
if (!fractionValue || parseInt(fractionValue, 10) === 0) {
// don't show 0's if fraction is 0
props = {
maximumFractionDigits: 0,
};
}
}
return props;
};
13 changes: 13 additions & 0 deletions src/payment/data/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
generateAndSubmitForm,
getOrderType,
transformResults,
getPropsToRemoveFractionZeroDigits,
} from './utils';

describe('modifyObjectKeys', () => {
Expand Down Expand Up @@ -240,3 +241,15 @@ describe('transformResults', () => {
});
});
});

describe('getPropsToRemoveFractionZeroDigits', () => {
it('should only hide fractional zeros when shouldRemoveFractionZeroDigits is false', () => {
expect(getPropsToRemoveFractionZeroDigits({ price: 79.00, shouldRemoveFractionZeroDigits: true })).toEqual({
maximumFractionDigits: 0,
});
expect(getPropsToRemoveFractionZeroDigits({ price: 79.00, shouldRemoveFractionZeroDigits: false })).toEqual({ });

expect(getPropsToRemoveFractionZeroDigits({ price: 79.43, shouldRemoveFractionZeroDigits: true })).toEqual({ });
expect(getPropsToRemoveFractionZeroDigits({ price: 79.43, shouldRemoveFractionZeroDigits: false })).toEqual({ });
});
});
3 changes: 2 additions & 1 deletion src/subscription/SubscriptionPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export const SubscriptionPage = () => {
}

if (errorCode) {
if (errorCode === 'empty_subscription') {
if (errorCode === 'empty_subscription'
|| errorCode === 'embargo-error') {
return (
<EmptyCartMessage />
);
Expand Down
4 changes: 2 additions & 2 deletions src/subscription/SubscriptionPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('<SubscriptionPage />', () => {
// verify that Checkout Form fields are present in the DOM
expect(screen.queryAllByText('Last Name (required)')).toHaveLength(1);
// verify that MonthlySubscriptionNotification is present in the DOM
expect(screen.queryAllByText('You’ll be charged $55.00 USD on April 21, 2025 then every 31 days until you cancel your subscription.')).toHaveLength(1);
expect(screen.queryAllByText('You’ll be charged $55 USD on April 21, 2025, then every 31 days until you cancel your subscription.')).toHaveLength(1);
});

it('should not render the Subscription details when error_code is present', () => {
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('<SubscriptionPage />', () => {
});

expect(screen.queryByText(/MX$1,050 */)).toBeNull();
expect(screen.getByText('$55.00/month USD after 7-day free trial')).toBeDefined();
expect(screen.getByText('$55/month USD after 7-day free trial')).toBeDefined();
});

it('should render a redirect spinner', () => {
Expand Down
9 changes: 2 additions & 7 deletions src/subscription/alerts/ErrorMessages.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import FallbackErrorMessage from '../../feedback/FallbackErrorMessage';
// eslint-disable-next-line import/prefer-default-export
export const EmptySubscriptionMessage = () => (
<FormattedMessage
id="subscription.messages.empty.subscription"
defaultMessage="You don't have any active subscription."
description="Notifies the user their they don't have any active subscriptions available."
/>
<FallbackErrorMessage />
);
4 changes: 4 additions & 0 deletions src/subscription/alerts/SubscriptionAlerts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
EmptySubscriptionMessage,
} from './ErrorMessages';

import { BasketChangedError } from '../../payment/AlertCodeMessages';

/**
* SubscriptionAlerts
* Reusable component to show server errors with i18n messages
Expand All @@ -13,6 +15,8 @@ export const SubscriptionAlerts = () => (
<AlertList
messageCodes={{
empty_subscription: (<EmptySubscriptionMessage />),
'embargo-error': (<EmptySubscriptionMessage />),
'basket-changed-error': (<BasketChangedError />),
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1759,7 +1759,7 @@ exports[`<SubscriptionCheckout /> should render the subscription checkout detail
<p
className="micro "
>
You’ll be charged $55.00 USD on April 21, 2025 then every 31 days until you cancel your subscription.
You’ll be charged $55 USD on April 21, 2025, then every 31 days until you cancel your subscription.
</p>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,46 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { useIntl, defineMessages } from '@edx/frontend-platform/i18n';
import { detailsSelector } from '../../data/details/selectors';
import { getPropsToRemoveFractionZeroDigits } from '../../../payment/data/utils';

const messages = defineMessages({
'subscription.checkout.billing.notification': {
id: 'subscription.checkout.billing.notification',
defaultMessage: 'You’ll be charged {price} {currency} on {trialEnd} then every 31 days until you cancel your subscription.',
defaultMessage: 'You’ll be charged {price} {currency} {trialEnd} then every 31 days until you cancel your subscription.',
description: 'Subscription monthly billing notification for Users that they will be charged every 31 days for this subscription.',
},
'subscription.checkout.billing.trial.date': {
id: 'subscription.checkout.billing.trial.date',
defaultMessage: 'on {date},',
description: 'Subscription legal trialing helping text.',
},
'subscription.checkout.billing.resubscribe.date': {
id: 'subscription.checkout.billing.resubscribe.date',
defaultMessage: 'today,',
description: 'Subscription legal resubscribe helping text.',
},
});

const MonthlyBillingNotification = () => {
const { price, currency, trialEnd } = useSelector(detailsSelector);
// TODO: render different text in case of resubscribe
const {
price, currency, trialEnd, isTrialEligible,
} = useSelector(detailsSelector);
const intl = useIntl();
const trialDateHelpingText = intl.formatMessage(messages['subscription.checkout.billing.trial.date'], { date: trialEnd });
const resubscribeDateHelpingText = intl.formatMessage(messages['subscription.checkout.billing.resubscribe.date'], {});

return (
<div className="d-flex justify-content-start pt-3 monthly-legal-notification">
<p className="micro ">
{
intl.formatMessage(messages['subscription.checkout.billing.notification'], {
currency,
trialEnd,
price: intl.formatNumber(price, { style: 'currency', currency: currency || 'USD' }),
trialEnd: isTrialEligible ? trialDateHelpingText : resubscribeDateHelpingText,
price: intl.formatNumber(price, {
style: 'currency',
currency: currency || 'USD',
...getPropsToRemoveFractionZeroDigits({ price, shouldRemoveFractionZeroDigits: true }),
}),
})
}
</p>
Expand Down
38 changes: 27 additions & 11 deletions src/subscription/confirmation-modal/ConfirmationModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,29 @@ import { subscriptionStatusSelector } from '../data/status/selectors';
import { detailsSelector } from '../data/details/selectors';

const messages = defineMessages({
'subscription.confirmation.modal.heading': {
id: 'subscription.confirmation.modal.heading',
'subscription.confirmation.modal.trialing.heading': {
id: 'subscription.confirmation.modal.trialing.heading',
defaultMessage: 'Congratulations! Your 7-day free trial of {programTitle} has started.',
description: 'Subscription confirmation success heading.',
description: 'Subscription trialing confirmation success heading.',
},
'subscription.confirmation.modal.body': {
id: 'subscription.confirmation.modal.body',
defaultMessage: "When your free trial ends, your subscription will begin, and we'll charge your payment method on file {price} per month. This subscription will automatically renew every month unless you cancel from the {ordersAndSubscriptionLink} page.",
description: 'Subscription confirmation success message explaining monthly subscription plan.',
'subscription.confirmation.modal.trialing.body': {
id: 'subscription.confirmation.modal.trialing.body',
defaultMessage: "When your free trial ends, your subscription will begin, and we'll charge your payment method on file {price} {currency} per month. To avoid being charged, you must cancel before your trial expires. This subscription will automatically renew every month unless you cancel from the {ordersAndSubscriptionLink} page.",
description: 'Subscription trialing confirmation success message explaining monthly subscription plan.',
},
'subscription.confirmation.modal.resubscribe.heading': {
id: 'subscription.confirmation.modal.resubscribe.heading',
defaultMessage: 'Congratulations! Your subscription to {programTitle} has started.',
description: 'Subscription resubscribe confirmation success heading.',
},
'subscription.confirmation.modal.resubscribe.body': {
id: 'subscription.confirmation.modal.resubscribe.body',
defaultMessage: 'We charged your payment method {price} {currency}. This subscription will be automatically renewed and charged monthly unless you cancel from the {ordersAndSubscriptionLink} page.',
description: 'Subscription resubscribe confirmation success message explaining monthly subscription plan.',
},
'subscription.confirmation.modal.body.orders.link': {
id: 'subscription.confirmation.modal.body.orders.link',
defaultMessage: 'Orders & Subscriptions',
defaultMessage: 'Orders and Subscriptions',
description: 'Subscription Orders & Subscriptions link placeholder.',
},
});
Expand All @@ -39,10 +49,12 @@ export const ConfirmationModal = () => {
price,
currency,
programUuid,
isTrialEligible,
} = useSelector(detailsSelector);
const intl = useIntl();
const { confirmationStatus } = useSelector(subscriptionStatusSelector);
const [isOpen, setOpen] = useState(false);
const subscriptionState = isTrialEligible ? 'trialing' : 'resubscribe';

useEffect(() => {
if (confirmationStatus === 'success') {
Expand Down Expand Up @@ -72,16 +84,20 @@ export const ConfirmationModal = () => {
<ModalDialog.Header>
<ModalDialog.Title as="h3">
{
intl.formatMessage(messages['subscription.confirmation.modal.heading'], {
intl.formatMessage(messages[`subscription.confirmation.modal.${subscriptionState}.heading`], {
programTitle,
})
}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{
intl.formatMessage(messages['subscription.confirmation.modal.body'], {
price: intl.formatNumber(price, { style: 'currency', currency: currency || 'USD' }),
intl.formatMessage(messages[`subscription.confirmation.modal.${subscriptionState}.body`], {
currency,
price: intl.formatNumber(price, {
style: 'currency',
currency: currency || 'USD',
}),
ordersAndSubscriptionLink,
})
}
Expand Down
Loading

0 comments on commit ecd4446

Please sign in to comment.