Skip to content

Commit

Permalink
feat(user): add functionality to switch offers
Browse files Browse the repository at this point in the history
  • Loading branch information
MelissaDTH authored and ChristiaanScheermeijer committed May 30, 2023
1 parent 790dba4 commit 2f01739
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/components/Payment/Payment.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@
margin-right: 4px;
}
}

.upgradeSubscription {
@include responsive.mobile-only() {
margin-bottom: 8px;
}
}
9 changes: 9 additions & 0 deletions src/components/Payment/Payment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ import subscription from '#test/fixtures/subscription.json';
import type { Customer } from '#types/account';
import type { PaymentDetail, Subscription, Transaction } from '#types/subscription';
import { renderWithRouter } from '#test/testUtils';
import * as checkoutController from '#src/stores/CheckoutController';

describe('<Payment>', () => {
afterEach(() => {
vi.clearAllMocks();
});

test('renders and matches snapshot', () => {
const spy = vi.spyOn(checkoutController, 'getSubscriptionSwitches');
spy.mockResolvedValue(undefined);

const { container } = renderWithRouter(
<Payment
accessModel="AVOD"
Expand All @@ -22,6 +30,7 @@ describe('<Payment>', () => {
canUpdatePaymentMethod={false}
showAllTransactions={false}
isLoading={false}
offerSwitchesAvailable={false}
/>,
);

Expand Down
27 changes: 25 additions & 2 deletions src/components/Payment/Payment.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';

import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint';
import { getSubscriptionSwitches } from '../../stores/CheckoutController';

import styles from './Payment.module.scss';

import TextField from '#components/TextField/TextField';
Expand All @@ -23,9 +26,11 @@ type Props = {
transactions: Transaction[] | null;
customer: Customer;
isLoading: boolean;
offerSwitchesAvailable: boolean;
panelClassName?: string;
panelHeaderClassName?: string;
onShowAllTransactionsClick?: () => void;
onUpgradeSubscriptionClick?: () => void;
showAllTransactions: boolean;
canUpdatePaymentMethod: boolean;
canRenewSubscription?: boolean;
Expand All @@ -44,13 +49,17 @@ const Payment = ({
showAllTransactions,
canRenewSubscription = false,
canUpdatePaymentMethod,
onUpgradeSubscriptionClick,
offerSwitchesAvailable,
}: Props): JSX.Element => {
const { t } = useTranslation(['user', 'account']);
const hiddenTransactionsCount = transactions ? transactions?.length - VISIBLE_TRANSACTIONS : 0;
const hasMoreTransactions = hiddenTransactionsCount > 0;
const navigate = useNavigate();
const location = useLocation();
const isGrantedSubscription = activeSubscription?.period === 'granted';
const breakpoint = useBreakpoint();
const isMobile = breakpoint === Breakpoint.xs;

function onCompleteSubscriptionClick() {
navigate(addQueryParam(location, 'u', 'choose-offer'));
Expand Down Expand Up @@ -81,6 +90,10 @@ const Payment = ({
}
}

useEffect(() => {
getSubscriptionSwitches();
}, []);

return (
<>
{accessModel === 'SVOD' && (
Expand All @@ -104,8 +117,18 @@ const Payment = ({
</p>
)}
</div>
{offerSwitchesAvailable && (
<Button
className={styles.upgradeSubscription}
label={t('user:payment.change_subscription')}
onClick={onUpgradeSubscriptionClick}
fullWidth={isMobile}
color="primary"
data-testid="change-subscription-button"
/>
)}
{activeSubscription.status === 'active' && !isGrantedSubscription ? (
<Button label={t('user:payment.cancel_subscription')} onClick={onCancelSubscriptionClick} />
<Button label={t('user:payment.cancel_subscription')} onClick={onCancelSubscriptionClick} fullWidth={isMobile} />
) : canRenewSubscription ? (
<Button label={t('user:payment.renew_subscription')} onClick={onRenewSubscriptionClick} />
) : null}
Expand Down
8 changes: 8 additions & 0 deletions src/containers/AccountModal/AccountModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import React from 'react';
import AccountModal from './AccountModal';

import { renderWithRouter } from '#test/testUtils';
import * as checkoutController from '#src/stores/CheckoutController';

describe('<AccountModal>', () => {
afterEach(() => {
vi.clearAllMocks();
});

test('renders and matches snapshot', () => {
const spy = vi.spyOn(checkoutController, 'getSubscriptionSwitches');
spy.mockResolvedValue(undefined);

const { container } = renderWithRouter(<AccountModal />);

expect(container).toMatchSnapshot();
Expand Down
2 changes: 2 additions & 0 deletions src/containers/AccountModal/AccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ const AccountModal = () => {
return <PersonalDetails />;
case 'choose-offer':
return <ChooseOffer />;
case 'upgrade-subscription':
return <ChooseOffer />;
case 'checkout':
return <Checkout />;
case 'payment-error':
Expand Down
51 changes: 45 additions & 6 deletions src/containers/AccountModal/forms/ChooseOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router';
import shallow from 'zustand/shallow';

import useQueryParam from '../../../hooks/useQueryParam';
import { switchSubscription } from '../../../stores/CheckoutController';
import { useAccountStore } from '../../../stores/AccountStore';

import useOffers from '#src/hooks/useOffers';
import { addQueryParam, removeQueryParam } from '#src/utils/location';
import { useCheckoutStore } from '#src/stores/CheckoutStore';
Expand All @@ -18,6 +22,11 @@ const ChooseOffer = () => {
const { t } = useTranslation('account');
const { setOffer } = useCheckoutStore(({ setOffer }) => ({ setOffer }), shallow);
const { isLoading, offerType, setOfferType, offers, offersDict, defaultOfferId, hasMultipleOfferTypes, hasPremierOffer } = useOffers();
const { subscription } = useAccountStore.getState();
const [offerSwitches, updateOffer] = useCheckoutStore((state) => [state.offerSwitches, state.updateOffer]);
const isOfferSwitch = useQueryParam('u') === 'upgrade-subscription';
const availableOffers = isOfferSwitch ? offerSwitches : offers;
const offerId = availableOffers[0]?.offerId || '';

const validationSchema: SchemaOf<ChooseOfferFormData> = object().shape({
offerId: mixed<string>().required(t('choose_offer.field_required')),
Expand All @@ -27,14 +36,37 @@ const ChooseOffer = () => {
offerId: defaultOfferId,
};

const determineSwitchDirection = () => {
const currentPeriod = subscription?.period;

if (currentPeriod === 'month') {
return 'upgrade';
} else if (currentPeriod === 'year') {
return 'downgrade';
} else {
return 'upgrade'; // Default to 'upgrade' if the period is not 'month' or 'year'
}
};

const chooseOfferSubmitHandler: UseFormOnSubmitHandler<ChooseOfferFormData> = async ({ offerId }, { setSubmitting, setErrors }) => {
const offer = offerId && offersDict[offerId];

if (!offer) return setErrors({ form: t('choose_offer.offer_not_found') });

setOffer(offer);
setSubmitting(false);
navigate(addQueryParam(location, 'u', 'checkout'));
if (isOfferSwitch) {
const targetOffer = offerSwitches.find((offer) => offer.offerId === offerId);
const targetOfferId = targetOffer?.offerId || '';

await switchSubscription(targetOfferId, determineSwitchDirection());
navigate(removeQueryParam(location, 'u'));
} else {
const selectedOffer = availableOffers.find((offer) => offer.offerId === offerId) || null;

setOffer(selectedOffer);
updateOffer(selectedOffer);
setSubmitting(false);
navigate(addQueryParam(location, 'u', 'checkout'));
}
};

const { handleSubmit, handleChange, setValue, values, errors, submitting } = useForm(initialValues, chooseOfferSubmitHandler, validationSchema);
Expand All @@ -45,8 +77,15 @@ const ChooseOffer = () => {
}, [isLoading, offers, location, navigate]);

useEffect(() => {
setValue('offerId', defaultOfferId);
}, [setValue, defaultOfferId]);
if (!isOfferSwitch) setValue('offerId', defaultOfferId);

// Update offerId if the user is switching offers to ensure the correct offer is checked in the ChooseOfferForm
// Initially, a defaultOfferId is set, but when switching offers, we need to use the id of the target offer
if (isOfferSwitch && values.offerId === initialValues.offerId) {
setValue('offerId', offerId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setValue, defaultOfferId, availableOffers]);

// loading state
if (!offers.length || isLoading) {
Expand All @@ -64,7 +103,7 @@ const ChooseOffer = () => {
values={values}
errors={errors}
submitting={submitting}
offers={offers}
offers={availableOffers}
offerType={offerType}
setOfferType={hasMultipleOfferTypes && !hasPremierOffer ? setOfferType : undefined}
/>
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en_US/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"annual_subscription": "Annual subscription",
"cancel_subscription": "Cancel subscription",
"card_number": "Card number",
"change_subscription": "Change subscription",
"complete_subscription": "Complete subscription",
"daily_subscription": "Daily subscription",
"expiry_date": "Expiry date",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/nl_NL/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"annual_subscription": "",
"cancel_subscription": "",
"card_number": "",
"change_subscription": "",
"complete_subscription": "",
"daily_subscription": "",
"expiry_date": "",
Expand Down
10 changes: 10 additions & 0 deletions src/pages/User/User.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useConfigStore } from '#src/stores/ConfigStore';
import type { Config } from '#types/Config';
import { useFavoritesStore } from '#src/stores/FavoritesStore';
import type { Playlist, PlaylistItem } from '#types/playlist';
import * as checkoutController from '#src/stores/CheckoutController';

const data = {
loading: false,
Expand Down Expand Up @@ -64,6 +65,15 @@ const data = {
};

describe('User Component tests', () => {
beforeEach(() => {
const spy = vi.spyOn(checkoutController, 'getSubscriptionSwitches');
spy.mockResolvedValue(undefined);
});

afterEach(() => {
vi.clearAllMocks();
});

test('Account Page', () => {
useAccountStore.setState(data);

Expand Down
13 changes: 12 additions & 1 deletion src/pages/User/User.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import shallow from 'zustand/shallow';

import { useCheckoutStore } from '../../stores/CheckoutStore';
import { addQueryParam } from '../../utils/location';

import styles from './User.module.scss';

import PlaylistContainer from '#src/containers/PlaylistContainer/PlaylistContainer';
Expand Down Expand Up @@ -48,13 +51,19 @@ const User = (): JSX.Element => {
canRenewSubscription,
canUpdatePaymentMethod,
} = useAccountStore();
const offerSwitches = useCheckoutStore((state) => state.offerSwitches);
const location = useLocation();

const onCardClick = (playlistItem: PlaylistItem) => navigate(mediaURL(playlistItem));
const onLogout = useCallback(async () => {
// Empty customer on a user page leads to navigate (code bellow), so we don't repeat it here
await logout();
}, []);

const handleUpgradeSubscriptionClick = async () => {
navigate(addQueryParam(location, 'u', 'upgrade-subscription'));
};

useEffect(() => {
if (!loading && !customer) {
navigate('/', { replace: true });
Expand Down Expand Up @@ -150,6 +159,8 @@ const User = (): JSX.Element => {
showAllTransactions={showAllTransactions}
canUpdatePaymentMethod={canUpdatePaymentMethod}
canRenewSubscription={canRenewSubscription}
onUpgradeSubscriptionClick={handleUpgradeSubscriptionClick}
offerSwitchesAvailable={!!offerSwitches.length}
/>
) : (
<Navigate to="my-account" />
Expand Down
15 changes: 15 additions & 0 deletions src/services/cleeng.checkout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import type {
GetOffers,
GetOrder,
GetPaymentMethods,
GetSubscriptionSwitches,
PaymentWithoutDetails,
PaymentWithPayPal,
SwitchSubscription,
UpdateOrder,
UpdatePaymentWithPayPal,
} from '#types/checkout';
Expand Down Expand Up @@ -88,6 +90,19 @@ export const paymentWithPayPal: PaymentWithPayPal = async (payload, sandbox, jwt
return post(sandbox, '/connectors/paypal/v1/tokens', JSON.stringify(paypalPayload), jwt);
};

export const getSubscriptionSwitches: GetSubscriptionSwitches = async (payload, sandbox, jwt) => {
return get(sandbox, `/customers/${payload.customerId}/subscription_switches/${payload.offerId}/availability`, jwt);
};

export const switchSubscription: SwitchSubscription = async (payload, sandbox, jwt) => {
return post(
sandbox,
`/customers/${payload.customerId}/subscription_switches/${payload.offerId}`,
JSON.stringify({ toOfferId: payload.toOfferId, switchDirection: payload.switchDirection }),
jwt,
);
};

export const getEntitlements: GetEntitlements = async (payload, sandbox, jwt = '') => {
return get(sandbox, `/entitlements/${payload.offerId}`, jwt);
};
Expand Down
Loading

0 comments on commit 2f01739

Please sign in to comment.