Skip to content

Commit 7dbd320

Browse files
authored
fix: adding trial plan subscription card changes (#1716)
* fix: adding trial plan subscription card changes * fix: nvm fix
1 parent 5a6b46b commit 7dbd320

File tree

15 files changed

+577
-80
lines changed

15 files changed

+577
-80
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
24
1+
24.11.0

src/components/learner-credit-management/data/hooks/tests/useBnrSubsidyRequests.test.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jest.mock('lodash-es', () => ({
2222

2323
jest.mock('../../../../../data/services/EnterpriseAccessApiService', () => ({
2424
fetchBnrSubsidyRequests: jest.fn(),
25-
fetchBnrSubsidyRequestsOverviw: jest.fn(),
25+
fetchBnrSubsidyRequestsOverview: jest.fn(),
2626
}));
2727

2828
jest.mock('../../utils', () => ({
@@ -125,7 +125,7 @@ describe('useBnrSubsidyRequests', () => {
125125
beforeEach(() => {
126126
jest.clearAllMocks();
127127
EnterpriseAccessApiService.fetchBnrSubsidyRequests.mockResolvedValue(mockApiResponse);
128-
EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverviw.mockResolvedValue(mockOverviewResponse);
128+
EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverview.mockResolvedValue(mockOverviewResponse);
129129
camelCaseObject.mockImplementation((data) => data);
130130
debounce.mockImplementation((fn) => fn);
131131
});

src/components/learner-credit-management/data/hooks/useBnrSubsidyRequests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ const useBnrSubsidyRequests = ({
224224
applyOverviewFiltersToOptions(filters, options);
225225
}
226226

227-
const response = await EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverviw(
227+
const response = await EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverview(
228228
enterpriseId,
229229
subsidyAccessPolicyId,
230230
options,
Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,106 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
2+
import { connect } from 'react-redux';
23
import PropTypes from 'prop-types';
4+
import { useIntl } from '@edx/frontend-platform/i18n';
5+
import { logError } from '@edx/frontend-platform/logging';
6+
import { camelCaseObject } from '@edx/frontend-platform/utils';
37
import {
4-
Row,
5-
Col,
8+
Col, Icon, Row, StatefulButton,
69
} from '@openedx/paragon';
10+
import { Error, Launch, SpinnerSimple } from '@openedx/paragon/icons';
711

812
import SubscriptionCard from './SubscriptionCard';
913
import { DEFAULT_LEAD_TEXT } from './data/constants';
14+
import EnterpriseAccessApiService from '../../data/services/EnterpriseAccessApiService';
1015

1116
const MultipleSubscriptionsPicker = ({
12-
leadText, subscriptions, createActions,
13-
}) => (
14-
<Row>
15-
<Col xs="12">
16-
<h2>Plans</h2>
17-
<p>{leadText}</p>
18-
</Col>
19-
<Col lg="10">
20-
{subscriptions.map(subscription => (
21-
<SubscriptionCard
22-
key={subscription.uuid}
23-
subscription={subscription}
24-
createActions={createActions}
25-
/>
26-
))}
27-
</Col>
28-
</Row>
29-
);
17+
enterpriseUuid, leadText, subscriptions, createActions,
18+
}) => {
19+
const intl = useIntl();
20+
const [stripeSessionStatus, setStripeSessionStatus] = useState('default');
21+
const hasSelfServiceSubs = subscriptions.some(sub => ['self-service-paid', 'self-service-trial'].includes(sub.planType));
22+
23+
const handleManageSubscriptionClick = async () => {
24+
setStripeSessionStatus('pending');
25+
try {
26+
const response = await EnterpriseAccessApiService.fetchStripeBillingPortalSession(enterpriseUuid);
27+
const results = camelCaseObject(response.data);
28+
if (results.url) {
29+
setStripeSessionStatus('default');
30+
window.open(results.url, '_blank', 'noopener,noreferrer');
31+
} else {
32+
setStripeSessionStatus('error');
33+
}
34+
} catch (error) {
35+
logError(error);
36+
setStripeSessionStatus('error');
37+
}
38+
};
39+
40+
return (
41+
<Row>
42+
<Col lg="10">
43+
<span className="d-flex justify-content-between">
44+
<h2>Plans</h2>
45+
{hasSelfServiceSubs && (
46+
<StatefulButton
47+
labels={{
48+
default: intl.formatMessage({
49+
id: 'subscriptions.manageSubscriptions.stripeLinkButton.default',
50+
defaultMessage: 'Manage subscription',
51+
description: 'Button text that link out to manage their subscriptions on the Stripe billing dashboard.',
52+
}),
53+
pending: intl.formatMessage({
54+
id: 'subscriptions.manageSubscriptions.stripeLinkButton.loading',
55+
defaultMessage: 'Creating Stripe session',
56+
description: 'Button text while we are creating a new Stripe billing session',
57+
}),
58+
error: intl.formatMessage({
59+
id: 'subscriptions.manageSubscriptions.stripeLinkButton.error',
60+
defaultMessage: 'Try again',
61+
description: 'Text for the button when creating a new Stripe session has failed',
62+
}),
63+
}}
64+
icons={{
65+
default: <Icon src={Launch} />,
66+
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
67+
error: <Icon src={Error} />,
68+
}}
69+
variant="outline-primary"
70+
state={stripeSessionStatus}
71+
onClick={handleManageSubscriptionClick}
72+
/>
73+
)}
74+
</span>
75+
<p>{leadText}</p>
76+
</Col>
77+
<Col lg="10">
78+
{subscriptions.map(subscription => (
79+
<SubscriptionCard
80+
key={subscription.uuid}
81+
subscription={subscription}
82+
createActions={createActions}
83+
/>
84+
))}
85+
</Col>
86+
</Row>
87+
);
88+
};
89+
90+
const mapStateToProps = state => ({
91+
enterpriseUuid: state.portalConfiguration.enterpriseId,
92+
});
3093

3194
MultipleSubscriptionsPicker.defaultProps = {
3295
leadText: DEFAULT_LEAD_TEXT,
3396
createActions: null,
3497
};
3598

3699
MultipleSubscriptionsPicker.propTypes = {
100+
enterpriseUuid: PropTypes.string.isRequired,
37101
leadText: PropTypes.string,
38102
subscriptions: PropTypes.arrayOf(PropTypes.shape()).isRequired,
39103
createActions: PropTypes.func,
40104
};
41105

42-
export default MultipleSubscriptionsPicker;
106+
export default connect(mapStateToProps)(MultipleSubscriptionsPicker);

src/components/subscriptions/MultipleSubscriptionsPage.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const MultipleSubscriptionsPage = ({
2727
leadText,
2828
createActions,
2929
}) => {
30-
const { loading, data } = useContext(SubscriptionContext);
30+
const { data, loading } = useContext(SubscriptionContext);
3131
const subscriptions = data.results;
3232

3333
if (loading) {

src/components/subscriptions/SubscriptionCard.jsx

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,50 @@
1-
import React from 'react';
1+
import React, { useContext } from 'react';
22
import PropTypes from 'prop-types';
3+
import { connect } from 'react-redux';
34
import dayjs from 'dayjs';
45
import { Link } from 'react-router-dom';
56
import {
6-
Card, Badge, Button, Stack, Row, Col,
7+
Badge, Button, Card, Col, Hyperlink, Row, Stack,
78
} from '@openedx/paragon';
9+
import { FormattedMessage, getLocale } from '@edx/frontend-platform/i18n';
810

911
import classNames from 'classnames';
10-
import { getSubscriptionStatus } from './data/utils';
11-
import { ACTIVE, SCHEDULED, SUBSCRIPTION_STATUS_BADGE_MAP } from './data/constants';
12+
import {
13+
ACTIVE, FREE_TRIAL_BADGE, TRIAL, SCHEDULED, SUBSCRIPTION_STATUS_BADGE_MAP, ENDED,
14+
} from './data/constants';
15+
import { useUpcomingInvoiceAmount } from './data/hooks';
16+
import { SubscriptionContext } from './SubscriptionData';
1217
import { ADMINISTER_SUBSCRIPTIONS_TARGETS } from '../ProductTours/AdminOnboardingTours/constants';
18+
import { makePlural } from '../../utils';
19+
import { getSubscriptionStatus, openStripeBillingPortal } from './data/utils';
1320

1421
const SubscriptionCard = ({
22+
enterpriseUuid,
1523
subscription,
1624
createActions,
1725
}) => {
1826
const {
19-
title,
20-
startDate,
2127
expirationDate,
2228
licenses = {},
29+
planType,
30+
startDate,
31+
title,
32+
uuid: subPlanUuid,
2333
} = subscription;
24-
34+
const { setErrors } = useContext(SubscriptionContext);
35+
const { invoiceAmount, currency, loadingStripeSummary } = useUpcomingInvoiceAmount(
36+
{ subPlanUuid, planType, setErrors },
37+
);
2538
const formattedStartDate = dayjs(startDate).format('MMMM D, YYYY');
2639
const formattedExpirationDate = dayjs(expirationDate).format('MMMM D, YYYY');
2740
const subscriptionStatus = getSubscriptionStatus(subscription);
2841

42+
let subscriptionUpcomingPrice;
43+
if (!loadingStripeSummary) {
44+
const locale = getLocale();
45+
subscriptionUpcomingPrice = `${invoiceAmount.toLocaleString(locale, { style: 'currency', currency, maximumFractionDigits: 0 })} ${currency.toUpperCase()}`;
46+
}
47+
2948
const renderDaysUntilPlanStartText = (className) => {
3049
if (!(subscriptionStatus === SCHEDULED)) {
3150
return null;
@@ -39,8 +58,8 @@ const SubscriptionCard = ({
3958
return (
4059
<span className={classNames('d-block small', className)}>
4160
Plan begins in {
42-
daysUntilPlanStart > 0 ? `${daysUntilPlanStart} day${daysUntilPlanStart > 1 ? 's' : ''}`
43-
: `${hoursUntilPlanStart} hour${hoursUntilPlanStart > 1 ? 's' : ''}`
61+
daysUntilPlanStart > 0 ? `${makePlural(daysUntilPlanStart, 'day')}`
62+
: `${makePlural(hoursUntilPlanStart, 'hour')}`
4463
}
4564
</span>
4665
);
@@ -69,14 +88,41 @@ const SubscriptionCard = ({
6988
const renderCardHeader = () => {
7089
const subtitle = (
7190
<div className="d-flex flex-wrap align-items-center">
72-
<Stack direction="horizontal" gap={3}>
73-
<Badge variant={SUBSCRIPTION_STATUS_BADGE_MAP[subscriptionStatus].variant}>
74-
{subscriptionStatus}
75-
</Badge>
91+
<Badge className="mr-2" variant={SUBSCRIPTION_STATUS_BADGE_MAP[subscriptionStatus].variant}>
92+
{subscriptionStatus}
93+
</Badge>
94+
{planType === TRIAL && (
95+
<>
96+
<Badge className="mr-2" variant="info">
97+
{FREE_TRIAL_BADGE}
98+
</Badge>
99+
{!(subscriptionStatus === ENDED) && (
100+
<FormattedMessage
101+
id="subscriptions.subscriptionCard.freeTrialDescription"
102+
defaultMessage="Your 14-day free trial will conclude on {boldDate}. Your paid subscription will automatically start, and the {subscriptionUpcomingPrice} subscription fee will be charged to the card on file. {stripeLink}"
103+
description="Message shown to warn customers with a free trial that they will be charged for the full subscription"
104+
values={{
105+
boldDate: <span className="ml-1 font-weight-bold">{formattedExpirationDate}</span>,
106+
subscriptionUpcomingPrice: <span className="ml-1 font-weight-bold">{subscriptionUpcomingPrice}</span>,
107+
stripeLink: (
108+
<Hyperlink
109+
className="ml-2"
110+
target="_blank"
111+
rel="noopener noreferrer"
112+
onClick={() => (openStripeBillingPortal(enterpriseUuid))}
113+
>
114+
Manage subscription
115+
</Hyperlink>),
116+
}}
117+
/>
118+
)}
119+
</>
120+
)}
121+
{planType !== TRIAL && (
76122
<span>
77123
{formattedStartDate} - {formattedExpirationDate}
78124
</span>
79-
</Stack>
125+
)}
80126
</div>
81127
);
82128

@@ -89,7 +135,8 @@ const SubscriptionCard = ({
89135
subtitle={subtitle}
90136
actions={(
91137
<div>
92-
{renderActions() || renderDaysUntilPlanStartText('mt-4')}
138+
{renderActions()}
139+
{renderDaysUntilPlanStartText('mt-4')}
93140
</div>
94141
)}
95142
/>
@@ -135,24 +182,31 @@ const SubscriptionCard = ({
135182
);
136183
};
137184

185+
const mapStateToProps = state => ({
186+
enterpriseUuid: state.portalConfiguration.enterpriseId,
187+
});
188+
138189
SubscriptionCard.defaultProps = {
139190
createActions: null,
140191
};
141192

142193
SubscriptionCard.propTypes = {
194+
enterpriseUuid: PropTypes.string.isRequired,
143195
subscription: PropTypes.shape({
144-
startDate: PropTypes.string.isRequired,
145196
expirationDate: PropTypes.string.isRequired,
146-
title: PropTypes.string.isRequired,
147197
licenses: PropTypes.shape({
148198
assigned: PropTypes.number.isRequired,
149199
activated: PropTypes.number.isRequired,
150200
allocated: PropTypes.number.isRequired,
151201
unassigned: PropTypes.number.isRequired,
152202
total: PropTypes.number.isRequired,
153203
}),
204+
planType: PropTypes.string.isRequired,
205+
startDate: PropTypes.string.isRequired,
206+
title: PropTypes.string.isRequired,
207+
uuid: PropTypes.string.isRequired,
154208
}).isRequired,
155209
createActions: PropTypes.func,
156210
};
157211

158-
export default SubscriptionCard;
212+
export default connect(mapStateToProps)(SubscriptionCard);

src/components/subscriptions/data/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const REVOKED = 'revoked';
1414
export const REVOCABLE_STATUSES = [ACTIVATED, ASSIGNED];
1515
export const ENROLLABLE_STATUSES = [ACTIVATED, ASSIGNED];
1616

17+
export const STRIPE_EVENT_SUMMARY = 'Stripe Event Summary';
1718
export const SUBSCRIPTIONS = 'Subscriptions';
1819
export const SUBSCRIPTION_USERS = 'Subscription Users';
1920
export const SUBSCRIPTION_USERS_OVERVIEW = 'Subscription Users Overview';
@@ -48,13 +49,17 @@ export const USER_STATUS_BADGE_MAP = {
4849
export const ACTIVE = 'Active';
4950
export const ENDED = 'Ended';
5051
export const SCHEDULED = 'Scheduled';
52+
export const TRIAL = 'Trial';
5153

5254
export const SUBSCRIPTION_STATUS_BADGE_MAP = {
5355
[ACTIVE]: { variant: 'primary' },
5456
[SCHEDULED]: { variant: 'secondary' },
5557
[ENDED]: { variant: 'light' },
58+
[TRIAL]: { variant: 'info' },
5659
};
5760

61+
export const FREE_TRIAL_BADGE = 'Free Trial';
62+
5863
// Browse and request constants `BrowseAndRequestAlert`
5964
export const BROWSE_AND_REQUEST_ALERT_COOKIE_PREFIX = 'dismissed-browse-and-request-alert';
6065

0 commit comments

Comments
 (0)