Skip to content

Commit

Permalink
feat: initial inplayer subscription change implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
naumovski-filip committed Jun 19, 2023
1 parent 035a0da commit b335b69
Show file tree
Hide file tree
Showing 18 changed files with 455 additions and 340 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"dependencies": {
"@adyen/adyen-web": "^5.42.1",
"@codeceptjs/allure-legacy": "^1.0.2",
"@inplayer-org/inplayer.js": "^3.13.12",
"@inplayer-org/inplayer.js": "^3.13.13",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",
"dompurify": "^2.3.8",
Expand Down
5 changes: 5 additions & 0 deletions public/locales/en/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@
"annual_subscription": "Annual subscription",
"cancel_subscription": "Cancel subscription",
"card_number": "Card number",
"change_plan": "",
"change_plan_error": "",
"change_subscription": "Change subscription",
"complete_subscription": "Complete subscription",
"current_plan": "",
"daily_subscription": "Daily subscription",
"downgrade_plan_success": "",
"expiry_date": "Expiry date",
"granted_subscription": "Granted subscription",
"hidden_transactions_one": "One more transaction",
Expand All @@ -94,6 +98,7 @@
"subscription_expires_on": "This plan will expire on {{date}}",
"transactions": "Transactions",
"update_payment_details": "Update payment details",
"upgrade_plan_success": "",
"weekly_subscription": "Weekly subscription"
}
}
5 changes: 5 additions & 0 deletions public/locales/es/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@
"annual_subscription": "Suscripción anual",
"cancel_subscription": "Cancelar suscripción",
"card_number": "Número de tarjeta",
"change_plan": "",
"change_plan_error": "",
"change_subscription": "Cambiar suscripción",
"complete_subscription": "Completar suscripción",
"current_plan": "",
"daily_subscription": "Suscripción diaria",
"downgrade_plan_success": "",
"expiry_date": "Fecha de vencimiento",
"granted_subscription": "Suscripción otorgada",
"hidden_transactions_one": "Una transacción más",
Expand All @@ -95,6 +99,7 @@
"subscription_expires_on": "Este plan expirará el {{date}}",
"transactions": "Transacciones",
"update_payment_details": "Actualizar detalles de pago",
"upgrade_plan_success": "",
"weekly_subscription": "Suscripción semanal"
}
}
62 changes: 62 additions & 0 deletions src/components/OfferSwitch/OfferSwitch.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@use 'src/styles/variables';
@use 'src/styles/theme';

.offerSwitchContainer {
display: flex;
align-items: center;
width: 100%;
height: auto;
padding: 16px;
gap: 25px;
color: variables.$gray-white;
background-color: theme.$panel-bg;
border-radius: 4px;
}

.activeOfferSwitchContainer {
color: variables.$gray-darker;
background-color: variables.$white;
}

.offerSwitchInfoContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
height: 100%;
gap: 4px;
font-weight: 600;
}

.offerSwitchPlanContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 2px;
}

.currentPlanHeading {
color: variables.$gray;
font-size: 10px;
}

.activeCurrentPlanHeading {
color: variables.$gray;
}

.nextBillingDate {
color: variables.$gray;
font-weight: 400;
font-size: 12px;
}

.price {
margin-left: auto;
font-size: 20px;
line-height: 28px;
}

.paymentFrequency {
font-size: 12px;
}
52 changes: 52 additions & 0 deletions src/components/OfferSwitch/OfferSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

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

import type { Offer } from '#types/checkout';
import Checkbox from '#components/Checkbox/Checkbox';
import { formatLocalizedDate, formatPrice } from '#src/utils/formatting';
import { useAccountStore } from '#src/stores/AccountStore';

interface OfferSwitchProps {
isCurrentOffer: boolean;
offer: Offer;
selected: {
value: boolean;
set: React.Dispatch<React.SetStateAction<string | null>>;
};
}

const OfferSwitch = ({ isCurrentOffer, offer, selected }: OfferSwitchProps) => {
const { t, i18n } = useTranslation('user');
const { customerPriceInclTax, customerCurrency, period } = offer;
const expiresAt = useAccountStore((state) => state.subscription?.expiresAt);

return (
<div className={classNames(styles.offerSwitchContainer, { [styles.activeOfferSwitchContainer]: selected.value })}>
<Checkbox name={offer.offerId} checked={selected.value} onChange={() => selected.set(offer.offerId)} />
<div className={styles.offerSwitchInfoContainer}>
{isCurrentOffer && (
<div className={classNames(styles.currentPlanHeading, { [styles.activeCurrentPlanHeading]: selected.value })}>{t('payment.current_plan')}</div>
)}
<div className={styles.offerSwitchPlanContainer}>
<div>{t(`payment.${period === 'month' ? 'monthly' : 'annual'}_subscription`)}</div>
{isCurrentOffer && expiresAt && (
<div className={styles.nextBillingDate}>
{t('payment.next_billing_date_on', { date: formatLocalizedDate(new Date(expiresAt * 1000), i18n.language) })}
</div>
)}
</div>
</div>
<div className={styles.price}>
{formatPrice(customerPriceInclTax, customerCurrency, undefined)}
{
//todo: i18n
}
<span className={styles.paymentFrequency}>/{period === 'month' ? 'month' : 'year'}</span>
</div>
</div>
);
};

export default OfferSwitch;
17 changes: 17 additions & 0 deletions src/components/Payment/Payment.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,20 @@
margin: 0 0 8px;
}
}

.changePlanContainer {
display: flex;
flex-direction: column;
gap: 24px;
}

.changePlanButtons {
display: flex;
flex-direction: row;
width: 100%;
gap: 12px;
}

.changePlanCancelButton {
margin-left: auto;
}
133 changes: 112 additions & 21 deletions src/components/Payment/Payment.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useMutation } from 'react-query';

import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint';
import IconButton from '../IconButton/IconButton';
Expand All @@ -18,6 +19,10 @@ import type { PaymentDetail, Subscription, Transaction } from '#types/subscripti
import type { AccessModel } from '#types/Config';
import PayPal from '#src/icons/PayPal';
import type { Offer } from '#types/checkout';
import useOffers from '#src/hooks/useOffers';
import OfferSwitch from '#components/OfferSwitch/OfferSwitch';
import { changeSubscription } from '#src/stores/CheckoutController';
import Alert from '#components/Alert/Alert';

const VISIBLE_TRANSACTIONS = 4;

Expand Down Expand Up @@ -69,6 +74,41 @@ const Payment = ({
const breakpoint = useBreakpoint();
const isMobile = breakpoint === Breakpoint.xs;

const { offers } = useOffers();
const hasSelectableOffers = offers.some((offer) => offer.planSwitchEnabled);

const [isChangingOffer, setIsChangingOffer] = useState(false);
const [selectedOfferId, setSelectedOfferId] = useState<string | null>(activeSubscription?.accessFeeId ?? null);
const [isUpgradeOffer, setIsUpgradeOffer] = useState<boolean | undefined>(undefined);

// TODO: debug why offer upgrade works but downgrade doesn't

useEffect(() => {
if (!isChangingOffer) {
setSelectedOfferId(activeSubscription?.accessFeeId ?? null);
}
}, [activeSubscription, isChangingOffer]);

useEffect(() => {
if (selectedOfferId && offers) {
setIsUpgradeOffer(
(offers.find((offer) => offer.offerId === selectedOfferId)?.customerPriceInclTax ?? 0) >
(offers.find((offer) => offer.offerId === activeSubscription?.accessFeeId)?.customerPriceInclTax ?? 0),
);
}
}, [selectedOfferId, offers, activeSubscription]);

const changeSubscriptionPlan = useMutation(changeSubscription);

const onChangePlanClick = async () => {
if (selectedOfferId && activeSubscription?.subscriptionId) {
changeSubscriptionPlan.mutate({
accessFeeId: selectedOfferId.slice(1),
subscriptionId: `${activeSubscription.subscriptionId}`,
});
}
};

function onCompleteSubscriptionClick() {
navigate(addQueryParam(location, 'u', 'choose-offer'));
}
Expand Down Expand Up @@ -98,43 +138,68 @@ const Payment = ({
}
}

const showChangeSubscriptionButton = offerSwitchesAvailable || (hasSelectableOffers && !isChangingOffer && activeSubscription?.status !== 'active_trial');

return (
<>
<Alert
isSuccess={changeSubscriptionPlan.isSuccess}
message={
changeSubscriptionPlan.isSuccess
? isUpgradeOffer
? t('user:payment.upgrade_plan_success')
: t('user:payment.downgrade_plan_success')
: t('user:payment.change_plan_error')
}
open={isChangingOffer && (changeSubscriptionPlan.isSuccess || changeSubscriptionPlan.isError)}
onClose={() => {
changeSubscriptionPlan.reset();
setIsChangingOffer(false);
}}
/>
{accessModel === 'SVOD' && (
<div className={panelClassName}>
<div className={panelHeaderClassName}>
<h3>{t('user:payment.subscription_details')}</h3>
<h3>{isChangingOffer ? t('user:payment.change_plan') : t('user:payment.subscription_details')}</h3>
</div>
{activeSubscription ? (
<React.Fragment>
<div className={styles.infoBox} key={activeSubscription.subscriptionId}>
<p>
<strong>{getTitle(activeSubscription.period)}</strong> <br />
{activeSubscription.status === 'active' && !isGrantedSubscription
? t('user:payment.next_billing_date_on', { date: formatLocalizedDate(new Date(activeSubscription.expiresAt * 1000), i18n.language) })
: t('user:payment.subscription_expires_on', { date: formatLocalizedDate(new Date(activeSubscription.expiresAt * 1000), i18n.language) })}
{pendingOffer && (
<span className={styles.pendingSwitch}>{t('user:payment.pending_offer_switch', { title: getTitle(pendingOffer.period) })}</span>
)}
</p>
{!isGrantedSubscription && (
<p className={styles.price}>
<strong>{formatPrice(activeSubscription.nextPaymentPrice, activeSubscription.nextPaymentCurrency, customer.country)}</strong>
<small>/{t(`account:periods.${activeSubscription.period}`)}</small>
{!isChangingOffer && (
<div className={styles.infoBox} key={activeSubscription.subscriptionId}>
<p>
<strong>{getTitle(activeSubscription.period)}</strong> <br />
{activeSubscription.status === 'active' && !isGrantedSubscription
? t('user:payment.next_billing_date_on', { date: formatLocalizedDate(new Date(activeSubscription.expiresAt * 1000), i18n.language) })
: t('user:payment.subscription_expires_on', { date: formatLocalizedDate(new Date(activeSubscription.expiresAt * 1000), i18n.language) })}
{pendingOffer && (
<span className={styles.pendingSwitch}>{t('user:payment.pending_offer_switch', { title: getTitle(pendingOffer.period) })}</span>
)}
</p>
)}
</div>
{offerSwitchesAvailable && (
{!isGrantedSubscription && (
<p className={styles.price}>
<strong>{formatPrice(activeSubscription.nextPaymentPrice, activeSubscription.nextPaymentCurrency, customer.country)}</strong>
<small>/{t(`account:periods.${activeSubscription.period}`)}</small>
</p>
)}
</div>
)}
{showChangeSubscriptionButton && (
<Button
className={styles.upgradeSubscription}
label={t('user:payment.change_subscription')}
onClick={onUpgradeSubscriptionClick}
onClick={() => {
if (offers.length > 1) {
setIsChangingOffer(true);
} else {
onUpgradeSubscriptionClick?.();
}
}}
fullWidth={isMobile}
color="primary"
data-testid="change-subscription-button"
/>
)}
{activeSubscription.status === 'active' && !isGrantedSubscription ? (
{activeSubscription.status === 'active' && !isGrantedSubscription && !isChangingOffer ? (
<Button label={t('user:payment.cancel_subscription')} onClick={onCancelSubscriptionClick} fullWidth={isMobile} />
) : canRenewSubscription ? (
<Button label={t('user:payment.renew_subscription')} onClick={onRenewSubscriptionClick} />
Expand All @@ -146,6 +211,32 @@ const Payment = ({
<Button variant="contained" color="primary" label={t('user:payment.complete_subscription')} onClick={onCompleteSubscriptionClick} />
</React.Fragment>
)}
{isChangingOffer && (
<div className={styles.changePlanContainer}>
{offers
.filter((o) => o.planSwitchEnabled)
.map((offer) => (
<OfferSwitch
key={offer.offerId}
isCurrentOffer={offer.offerId === activeSubscription?.accessFeeId}
offer={offer}
selected={{ value: selectedOfferId === offer.offerId, set: setSelectedOfferId }}
/>
))}
<div className={styles.changePlanButtons}>
<Button label={t('user:account.save')} onClick={onChangePlanClick} disabled={changeSubscriptionPlan.isLoading} />
<Button label={t('user:account.cancel')} onClick={() => setIsChangingOffer(false)} variant="text" />
{activeSubscription?.status !== 'cancelled' && (
<Button
className={styles.changePlanCancelButton}
label={t('user:payment.cancel_subscription')}
onClick={onCancelSubscriptionClick}
variant="danger"
/>
)}
</div>
</div>
)}
</div>
)}
<div className={panelClassName}>
Expand Down
Loading

0 comments on commit b335b69

Please sign in to comment.