diff --git a/package.json b/package.json index e0352847c..6f3476e41 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/en/user.json b/public/locales/en/user.json index 0b83411c0..36ca71656 100644 --- a/public/locales/en/user.json +++ b/public/locales/en/user.json @@ -70,9 +70,14 @@ "billing_history": "Billing history", "cancel_subscription": "Cancel subscription", "card_number": "Card number", + "change_plan": "Change Plan", + "change_plan_error": "There was a problem saving your subscription plan change.", "change_subscription": "Change subscription", "complete_subscription": "Complete subscription", + "current_plan": "CURRENT PLAN", "daily_subscription": "Daily subscription", + "downgrade_on": "Pending downgrade on {{date}}", + "downgrade_plan_success": "You've successfully changed the subscription plan. Your new plan will start after the end of the current cycle ({{date}}).", "expiry_date": "Expiry date", "granted_subscription": "Granted subscription", "hidden_transactions_one": "One more transaction", @@ -85,6 +90,7 @@ "no_transactions": "No transactions", "other": "other", "payment_method": "Payment method", + "pending_downgrade": "Pending downgrade", "pending_offer_switch": "Will update to a \"{{title}}\" after the next billing date", "price_paid_with": "{{price}} paid with {{method}}", "price_paid_with_card": "Price paid with card", @@ -95,6 +101,7 @@ "subscription_details": "Subscription details", "subscription_expires_on": "This plan will expire on {{date}}", "update_payment_details": "Update payment details", + "upgrade_plan_success": "You've successfully changed the subscription plan. You can enjoy your additional benefits immediately.", "weekly_subscription": "Weekly subscription" } } diff --git a/public/locales/es/user.json b/public/locales/es/user.json index dcdce36db..46547d523 100644 --- a/public/locales/es/user.json +++ b/public/locales/es/user.json @@ -70,9 +70,14 @@ "billing_history": "Historial de facturación", "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_on": "", + "downgrade_plan_success": "", "expiry_date": "Fecha de vencimiento", "granted_subscription": "Suscripción otorgada", "hidden_transactions_one": "Una transacción más", @@ -86,6 +91,7 @@ "no_transactions": "No hay transacciones", "other": "otro", "payment_method": "Método de pago", + "pending_downgrade": "", "pending_offer_switch": "Se actualizará a \"{{title}}\" después de la próxima fecha de facturación", "price_paid_with": "{{price}} pagado con {{method}}", "price_paid_with_card": "Precio pagado con tarjeta", @@ -96,6 +102,7 @@ "subscription_details": "Detalles de la suscripción", "subscription_expires_on": "Este plan expirará el {{date}}", "update_payment_details": "Actualizar detalles de pago", + "upgrade_plan_success": "", "weekly_subscription": "Suscripción semanal" } } diff --git a/src/components/OfferSwitch/OfferSwitch.module.scss b/src/components/OfferSwitch/OfferSwitch.module.scss new file mode 100644 index 000000000..f55259025 --- /dev/null +++ b/src/components/OfferSwitch/OfferSwitch.module.scss @@ -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; +} diff --git a/src/components/OfferSwitch/OfferSwitch.tsx b/src/components/OfferSwitch/OfferSwitch.tsx new file mode 100644 index 000000000..ecca4ead0 --- /dev/null +++ b/src/components/OfferSwitch/OfferSwitch.tsx @@ -0,0 +1,55 @@ +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'; + +type OfferSwitchProps = { + isCurrentOffer: boolean; + pendingDowngradeOfferId: string; + offer: Offer; + selected: boolean; + onChange: (offerId: string) => void; + expiresAt: number | undefined; +}; + +const OfferSwitch = ({ isCurrentOffer, pendingDowngradeOfferId, offer, selected, onChange, expiresAt }: OfferSwitchProps) => { + const { t, i18n } = useTranslation(['user', 'account']); + const { customerPriceInclTax, customerCurrency, period } = offer; + + const isPendingDowngrade = pendingDowngradeOfferId === offer.offerId; + + return ( +
+ onChange(offer.offerId)} /> +
+ {(isCurrentOffer || isPendingDowngrade) && ( +
+ {isCurrentOffer && t('user:payment.current_plan').toUpperCase()} + {isPendingDowngrade && t('user:payment.pending_downgrade').toUpperCase()} +
+ )} +
+
{t(`user:payment.${period === 'month' ? 'monthly' : 'annual'}_subscription`)}
+ {(isCurrentOffer || isPendingDowngrade) && expiresAt && ( +
+ {isCurrentOffer && + !pendingDowngradeOfferId && + t('user:payment.next_billing_date_on', { date: formatLocalizedDate(new Date(expiresAt * 1000), i18n.language) })} + {isPendingDowngrade && t('user:payment.downgrade_on', { date: formatLocalizedDate(new Date(expiresAt * 1000), i18n.language) })} +
+ )} +
+
+
+ {formatPrice(customerPriceInclTax, customerCurrency, undefined)} + /{t(`account:periods.${period}_one`)} +
+
+ ); +}; + +export default OfferSwitch; diff --git a/src/components/Payment/Payment.module.scss b/src/components/Payment/Payment.module.scss index 08a6daa95..478de3494 100644 --- a/src/components/Payment/Payment.module.scss +++ b/src/components/Payment/Payment.module.scss @@ -83,3 +83,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; +} diff --git a/src/components/Payment/Payment.test.tsx b/src/components/Payment/Payment.test.tsx index eeae8a57c..511a85c7f 100644 --- a/src/components/Payment/Payment.test.tsx +++ b/src/components/Payment/Payment.test.tsx @@ -25,6 +25,19 @@ describe('', () => { isLoading={false} offerSwitchesAvailable={false} onShowReceiptClick={vi.fn()} + onUpgradeSubscriptionClick={vi.fn()} + onShowAllTransactionsClick={vi.fn()} + changeSubscriptionPlan={{ + isLoading: false, + isSuccess: false, + isError: false, + reset: vi.fn(), + }} + onChangePlanClick={vi.fn()} + selectedOfferId={null} + setSelectedOfferId={vi.fn()} + isUpgradeOffer={undefined} + setIsUpgradeOffer={vi.fn()} />, ); diff --git a/src/components/Payment/Payment.tsx b/src/components/Payment/Payment.tsx index e445166a5..73913bad8 100644 --- a/src/components/Payment/Payment.tsx +++ b/src/components/Payment/Payment.tsx @@ -1,23 +1,25 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint'; -import IconButton from '../IconButton/IconButton'; import ExternalLink from '../../icons/ExternalLink'; +import IconButton from '../IconButton/IconButton'; import styles from './Payment.module.scss'; -import TextField from '#components/TextField/TextField'; -import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; +import Alert from '#components/Alert/Alert'; import Button from '#components/Button/Button'; -import type { Customer } from '#types/account'; +import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; +import OfferSwitch from '#components/OfferSwitch/OfferSwitch'; +import TextField from '#components/TextField/TextField'; +import PayPal from '#src/icons/PayPal'; import { formatLocalizedDate, formatPrice } from '#src/utils/formatting'; import { addQueryParam } from '#src/utils/location'; -import type { PaymentDetail, Subscription, Transaction } from '#types/subscription'; import type { AccessModel } from '#types/Config'; -import PayPal from '#src/icons/PayPal'; +import type { Customer } from '#types/account'; import type { Offer } from '#types/checkout'; +import type { PaymentDetail, Subscription, Transaction } from '#types/subscription'; const VISIBLE_TRANSACTIONS = 4; @@ -39,6 +41,19 @@ type Props = { canUpdatePaymentMethod: boolean; canRenewSubscription?: boolean; canShowReceipts?: boolean; + offers?: Offer[]; + pendingDowngradeOfferId?: string; + changeSubscriptionPlan: { + isLoading: boolean; + isError: boolean; + isSuccess: boolean; + reset: () => void; + }; + onChangePlanClick: () => void; + selectedOfferId: string | null; + setSelectedOfferId: (offerId: string | null) => void; + isUpgradeOffer: boolean | undefined; + setIsUpgradeOffer: (isUpgradeOffer: boolean | undefined) => void; }; const Payment = ({ @@ -59,6 +74,14 @@ const Payment = ({ canUpdatePaymentMethod, onUpgradeSubscriptionClick, offerSwitchesAvailable, + offers = [], + pendingDowngradeOfferId = '', + changeSubscriptionPlan, + onChangePlanClick, + selectedOfferId, + setSelectedOfferId, + isUpgradeOffer, + setIsUpgradeOffer, }: Props): JSX.Element => { const { t, i18n } = useTranslation(['user', 'account']); const hiddenTransactionsCount = transactions ? transactions?.length - VISIBLE_TRANSACTIONS : 0; @@ -69,6 +92,27 @@ const Payment = ({ const breakpoint = useBreakpoint(); const isMobile = breakpoint === Breakpoint.xs; + const [isChangingOffer, setIsChangingOffer] = useState(false); + + useEffect(() => { + if (!isChangingOffer) { + setSelectedOfferId(activeSubscription?.accessFeeId ?? null); + } + }, [activeSubscription, isChangingOffer, setSelectedOfferId]); + + useEffect(() => { + setIsChangingOffer(false); + }, [activeSubscription?.status, activeSubscription?.pendingSwitchId]); + + 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, setIsUpgradeOffer]); + function onCompleteSubscriptionClick() { navigate(addQueryParam(location, 'u', 'choose-offer')); } @@ -101,44 +145,84 @@ const Payment = ({ } } + const showChangeSubscriptionButton = offerSwitchesAvailable || (!isChangingOffer && !canRenewSubscription); + return ( <> + { + changeSubscriptionPlan.reset(); + setIsChangingOffer(false); + }} + /> {accessModel === 'SVOD' && (
-

{t('user:payment.subscription_details')}

+

{isChangingOffer ? t('user:payment.change_plan') : t('user:payment.subscription_details')}

{activeSubscription ? ( -
-

- {getTitle(activeSubscription.period)}
- {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 && ( - {t('user:payment.pending_offer_switch', { title: getTitle(pendingOffer.period) })} - )} -

- {!isGrantedSubscription && ( -

- {formatPrice(activeSubscription.nextPaymentPrice, activeSubscription.nextPaymentCurrency, customer.country)} - /{t(`account:periods.${activeSubscription.period}`)} + {!isChangingOffer && ( +

+

+ {getTitle(activeSubscription.period)}
+ {activeSubscription.status === 'active' && !isGrantedSubscription && !pendingDowngradeOfferId + ? 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 || pendingDowngradeOfferId) && activeSubscription.status !== 'cancelled' && ( + + {t('user:payment.pending_offer_switch', { + title: getTitle(pendingOffer?.period || offers.find((offer) => offer.offerId === pendingDowngradeOfferId)?.period || 'month'), + })} + + )}

- )} -
- {offerSwitchesAvailable && ( + {!isGrantedSubscription && ( +

+ {formatPrice(activeSubscription.nextPaymentPrice, activeSubscription.nextPaymentCurrency, customer.country)} + /{t(`account:periods.${activeSubscription.period}`)} +

+ )} +
+ )} + {showChangeSubscriptionButton && (
+ + )} )}
diff --git a/src/containers/PaymentContainer/PaymentContainer.tsx b/src/containers/PaymentContainer/PaymentContainer.tsx new file mode 100644 index 000000000..e3dc0f49d --- /dev/null +++ b/src/containers/PaymentContainer/PaymentContainer.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router'; +import shallow from 'zustand/shallow'; + +import styles from '#src/pages/User/User.module.scss'; +import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; +import Payment from '#components/Payment/Payment'; +import { getReceipt } from '#src/stores/AccountController'; +import { useAccountStore } from '#src/stores/AccountStore'; +import { getSubscriptionSwitches } from '#src/stores/CheckoutController'; +import { useCheckoutStore } from '#src/stores/CheckoutStore'; +import { useConfigStore } from '#src/stores/ConfigStore'; +import { addQueryParam } from '#src/utils/location'; +import useOffers from '#src/hooks/useOffers'; +import { useSubscriptionChange } from '#src/hooks/useSubscriptionChange'; + +const PaymentContainer = () => { + const { accessModel } = useConfigStore( + (s) => ({ + accessModel: s.accessModel, + favoritesList: s.config.features?.favoritesList, + }), + shallow, + ); + const navigate = useNavigate(); + + const { + user: customer, + subscription: activeSubscription, + transactions, + activePayment, + pendingOffer, + loading, + canRenewSubscription, + canUpdatePaymentMethod, + canShowReceipts, + } = useAccountStore(); + + const [showAllTransactions, setShowAllTransactions] = useState(false); + const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); + const [selectedOfferId, setSelectedOfferId] = useState(activeSubscription?.accessFeeId ?? null); + const [isUpgradeOffer, setIsUpgradeOffer] = useState(undefined); + + const { offerSwitches } = useCheckoutStore(); + const location = useLocation(); + + const handleUpgradeSubscriptionClick = async () => { + navigate(addQueryParam(location, 'u', 'upgrade-subscription')); + }; + + const handleShowReceiptClick = async (transactionId: string) => { + setIsLoadingReceipt(true); + + try { + const receipt = await getReceipt(transactionId); + + if (receipt) { + const newWindow = window.open('', `Receipt ${transactionId}`, ''); + const htmlString = window.atob(receipt); + + if (newWindow) { + newWindow.opener = null; + newWindow.document.write(htmlString); + newWindow.document.close(); + } + } + } catch (error: unknown) { + throw new Error("Couldn't parse receipt. " + (error instanceof Error ? error.message : '')); + } + + setIsLoadingReceipt(false); + }; + + useEffect(() => { + if (accessModel !== 'AVOD') { + getSubscriptionSwitches(); + } + }, [accessModel]); + + useEffect(() => { + if (!loading && !customer) { + navigate('/', { replace: true }); + } + }, [navigate, customer, loading]); + + const { offers } = useOffers(); + + const changeSubscriptionPlan = useSubscriptionChange(isUpgradeOffer ?? false, selectedOfferId, customer, activeSubscription?.subscriptionId); + + const onChangePlanClick = async () => { + if (selectedOfferId && activeSubscription?.subscriptionId) { + changeSubscriptionPlan.mutate({ + accessFeeId: selectedOfferId.slice(1), + subscriptionId: `${activeSubscription.subscriptionId}`, + }); + } + }; + + if (!customer) { + return ; + } + + const pendingDowngradeOfferId = (customer.metadata?.[`${activeSubscription?.subscriptionId}_pending_downgrade`] as string) || ''; + + return ( + setShowAllTransactions(true)} + showAllTransactions={showAllTransactions} + canUpdatePaymentMethod={canUpdatePaymentMethod} + canRenewSubscription={canRenewSubscription} + onUpgradeSubscriptionClick={handleUpgradeSubscriptionClick} + offerSwitchesAvailable={!!offerSwitches.length} + canShowReceipts={canShowReceipts} + onShowReceiptClick={handleShowReceiptClick} + offers={offers} + pendingDowngradeOfferId={pendingDowngradeOfferId} + changeSubscriptionPlan={changeSubscriptionPlan} + onChangePlanClick={onChangePlanClick} + selectedOfferId={selectedOfferId} + setSelectedOfferId={(offerId: string | null) => setSelectedOfferId(offerId)} + isUpgradeOffer={isUpgradeOffer} + setIsUpgradeOffer={(isUpgradeOffer: boolean | undefined) => setIsUpgradeOffer(isUpgradeOffer)} + /> + ); +}; + +export default PaymentContainer; diff --git a/src/hooks/useOffers.ts b/src/hooks/useOffers.ts index 62c0eff15..74a93123b 100644 --- a/src/hooks/useOffers.ts +++ b/src/hooks/useOffers.ts @@ -16,7 +16,6 @@ const useOffers = () => { const { clientOffers, sandbox } = useClientIntegration(); const checkoutService: CheckoutService = useService(({ checkoutService }) => checkoutService); - if (!checkoutService) throw new Error('checkout service is not available'); const { requestedMediaOffers } = useCheckoutStore(({ requestedMediaOffers }) => ({ requestedMediaOffers }), shallow); const hasTvodOffer = (requestedMediaOffers || []).some((offer) => offer.offerId); @@ -28,7 +27,7 @@ const useOffers = () => { return [...(requestedMediaOffers || []).map(({ offerId }) => offerId), ...clientOffers].filter(Boolean); }, [requestedMediaOffers, clientOffers]); - const { data: allOffers, isLoading } = useQuery(['offers', offerIds.join('-')], () => checkoutService.getOffers({ offerIds }, sandbox)); + const { data: allOffers, isLoading } = useQuery(['offers', offerIds.join('-')], () => checkoutService?.getOffers({ offerIds }, sandbox)); // The `offerQueries` variable mutates on each render which prevents the useMemo to work properly. return useMemo(() => { diff --git a/src/hooks/useSubscriptionChange.ts b/src/hooks/useSubscriptionChange.ts new file mode 100644 index 000000000..fa1bf6209 --- /dev/null +++ b/src/hooks/useSubscriptionChange.ts @@ -0,0 +1,35 @@ +import { useMutation } from 'react-query'; + +import { updateUser } from '#src/stores/AccountController'; +import { useAccountStore } from '#src/stores/AccountStore'; +import { changeSubscription } from '#src/stores/CheckoutController'; +import type { Customer } from '#types/account'; + +export const useSubscriptionChange = ( + isUpgradeOffer: boolean, + selectedOfferId: string | null, + customer: Customer | null, + activeSubscriptionId: string | number | undefined, +) => { + const updateSubscriptionMetadata = useMutation(updateUser, { + onSuccess: () => { + useAccountStore.setState({ + loading: false, + }); + }, + }); + + return useMutation(changeSubscription, { + onSuccess: () => { + if (!isUpgradeOffer && selectedOfferId) { + updateSubscriptionMetadata.mutate({ + firstName: customer?.firstName || '', + lastName: customer?.lastName || '', + metadata: { + [`${activeSubscriptionId}_pending_downgrade`]: selectedOfferId, + }, + }); + } + }, + }); +}; diff --git a/src/pages/User/User.tsx b/src/pages/User/User.tsx index e1068222c..fb0d9caab 100644 --- a/src/pages/User/User.tsx +++ b/src/pages/User/User.tsx @@ -1,31 +1,29 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'; import shallow from 'zustand/shallow'; import styles from './User.module.scss'; +import AccountComponent from '#components/Account/Account'; +import Button from '#components/Button/Button'; +import ConfirmationDialog from '#components/ConfirmationDialog/ConfirmationDialog'; +import Favorites from '#components/Favorites/Favorites'; +import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; +import PaymentContainer from '#src/containers/PaymentContainer/PaymentContainer'; import PlaylistContainer from '#src/containers/PlaylistContainer/PlaylistContainer'; -import { mediaURL } from '#src/utils/formatting'; +import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint'; import AccountCircle from '#src/icons/AccountCircle'; -import Favorite from '#src/icons/Favorite'; import BalanceWallet from '#src/icons/BalanceWallet'; import Exit from '#src/icons/Exit'; +import Favorite from '#src/icons/Favorite'; +import { logout } from '#src/stores/AccountController'; import { useAccountStore } from '#src/stores/AccountStore'; +import { getSubscriptionSwitches } from '#src/stores/CheckoutController'; import { PersonalShelf, useConfigStore } from '#src/stores/ConfigStore'; -import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint'; -import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; -import ConfirmationDialog from '#components/ConfirmationDialog/ConfirmationDialog'; -import Payment from '#components/Payment/Payment'; -import AccountComponent from '#components/Account/Account'; -import Button from '#components/Button/Button'; -import Favorites from '#components/Favorites/Favorites'; -import type { PlaylistItem } from '#types/playlist'; -import { getReceipt, logout } from '#src/stores/AccountController'; import { clear as clearFavorites } from '#src/stores/FavoritesController'; -import { getSubscriptionSwitches } from '#src/stores/CheckoutController'; -import { useCheckoutStore } from '#src/stores/CheckoutStore'; -import { addQueryParam } from '#src/utils/location'; +import { mediaURL } from '#src/utils/formatting'; +import type { PlaylistItem } from '#types/playlist'; const User = (): JSX.Element => { const { accessModel, favoritesList } = useConfigStore( @@ -39,24 +37,9 @@ const User = (): JSX.Element => { const { t } = useTranslation('user'); const breakpoint = useBreakpoint(); const [clearFavoritesOpen, setClearFavoritesOpen] = useState(false); - const [showAllTransactions, setShowAllTransactions] = useState(false); - const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); const isLargeScreen = breakpoint > Breakpoint.md; - const { - user: customer, - subscription, - transactions, - activePayment, - pendingOffer, - loading, - canUpdateEmail, - canRenewSubscription, - canUpdatePaymentMethod, - canShowReceipts, - } = useAccountStore(); - const offerSwitches = useCheckoutStore((state) => state.offerSwitches); - const location = useLocation(); + const { user: customer, subscription, loading, canUpdateEmail } = useAccountStore(); const onCardClick = (playlistItem: PlaylistItem) => navigate(mediaURL({ media: playlistItem })); const onLogout = useCallback(async () => { @@ -64,33 +47,6 @@ const User = (): JSX.Element => { await logout(); }, []); - const handleUpgradeSubscriptionClick = async () => { - navigate(addQueryParam(location, 'u', 'upgrade-subscription')); - }; - - const handleShowReceiptClick = async (transactionId: string) => { - setIsLoadingReceipt(true); - - try { - const receipt = await getReceipt(transactionId); - - if (receipt) { - const newWindow = window.open('', `Receipt ${transactionId}`, ''); - const htmlString = window.atob(receipt); - - if (newWindow) { - newWindow.opener = null; - newWindow.document.write(htmlString); - newWindow.document.close(); - } - } - } catch (error: unknown) { - throw new Error("Couldn't parse receipt. " + (error instanceof Error ? error.message : '')); - } - - setIsLoadingReceipt(false); - }; - useEffect(() => { if (accessModel !== 'AVOD') { getSubscriptionSwitches(); @@ -175,34 +131,7 @@ const User = (): JSX.Element => { } /> )} - setShowAllTransactions(true)} - showAllTransactions={showAllTransactions} - canUpdatePaymentMethod={canUpdatePaymentMethod} - canRenewSubscription={canRenewSubscription} - onUpgradeSubscriptionClick={handleUpgradeSubscriptionClick} - offerSwitchesAvailable={!!offerSwitches.length} - canShowReceipts={canShowReceipts} - onShowReceiptClick={handleShowReceiptClick} - /> - ) : ( - - ) - } - /> + : } /> } />
diff --git a/src/pages/User/__snapshots__/User.test.tsx.snap b/src/pages/User/__snapshots__/User.test.tsx.snap index 1aec12768..108a913e0 100644 --- a/src/pages/User/__snapshots__/User.test.tsx.snap +++ b/src/pages/User/__snapshots__/User.test.tsx.snap @@ -636,6 +636,7 @@ exports[`User Component tests > Payments Page 1`] = `
user:payment.next_billing_date_on +

Payments Page 1`] = `

diff --git a/src/services/inplayer.account.service.ts b/src/services/inplayer.account.service.ts index 44bd17fa2..0d4be5d48 100644 --- a/src/services/inplayer.account.service.ts +++ b/src/services/inplayer.account.service.ts @@ -423,12 +423,14 @@ function formatUpdateAccount(customer: UpdateCustomerArgs) { const firstName = customer.firstName?.trim() || ''; const lastName = customer.lastName?.trim() || ''; const fullName = `${firstName} ${lastName}`.trim() || (customer.email as string); + const metadata: { [key: string]: string } = { + first_name: firstName, + surname: lastName, + ...customer.metadata, + }; const data: UpdateAccountData = { fullName, - metadata: { - first_name: firstName, - surname: lastName, - }, + metadata, }; return data; diff --git a/src/services/inplayer.checkout.service.ts b/src/services/inplayer.checkout.service.ts index 9d509e390..84c00da9c 100644 --- a/src/services/inplayer.checkout.service.ts +++ b/src/services/inplayer.checkout.service.ts @@ -208,6 +208,7 @@ const formatOffer = (offer: AccessFee): Offer => { active: true, period: offer.access_type.period === 'month' && offer.access_type.quantity === 12 ? 'year' : offer.access_type.period, freePeriods: offer.trial_period ? 1 : 0, + planSwitchEnabled: offer.item.plan_switch_enabled ?? false, } as Offer; }; diff --git a/src/services/inplayer.subscription.service.ts b/src/services/inplayer.subscription.service.ts index a90f207ab..4dc9e3e6c 100644 --- a/src/services/inplayer.subscription.service.ts +++ b/src/services/inplayer.subscription.service.ts @@ -1,7 +1,7 @@ import i18next from 'i18next'; import InPlayer, { PurchaseDetails, Card, GetItemAccessV1, SubscriptionDetails as InplayerSubscription } from '@inplayer-org/inplayer.js'; -import type { PaymentDetail, Subscription, Transaction, UpdateCardDetails, UpdateSubscription } from '#types/subscription'; +import type { ChangeSubscription, PaymentDetail, Subscription, Transaction, UpdateCardDetails, UpdateSubscription } from '#types/subscription'; import type { Config } from '#types/Config'; import type { InPlayerError } from '#types/inplayer'; @@ -17,6 +17,7 @@ interface SubscriptionDetails extends InplayerSubscription { access_type?: { period: string; }; + access_fee_id?: number; } export async function getActiveSubscription({ config }: { config: Config }) { @@ -94,6 +95,18 @@ export const updateSubscription: UpdateSubscription = async ({ offerId, unsubscr } }; +export const changeSubscription: ChangeSubscription = async ({ accessFeeId, subscriptionId }) => { + try { + const response = await InPlayer.Subscription.changeSubscriptionPlan({ access_fee_id: parseInt(accessFeeId), inplayer_token: subscriptionId }); + return { + errors: [], + responseData: { message: response.data.message }, + }; + } catch { + throw new Error('Failed to change subscription'); + } +}; + export const updateCardDetails: UpdateCardDetails = async ({ cardName, cardNumber, cvc, expMonth, expYear, currency }) => { try { const response = await InPlayer.Payment.setDefaultCreditCard({ cardName, cardNumber, cvc, expMonth, expYear, currency }); @@ -103,6 +116,7 @@ export const updateCardDetails: UpdateCardDetails = async ({ cardName, cardNumbe throw new Error('Failed to update card details'); } }; + const formatCardDetails = (card: Card & { card_type: string; account_id: number; currency: string }): PaymentDetail => { const { number, exp_month, exp_year, card_name, card_type, account_id, currency } = card; const zeroFillExpMonth = `0${exp_month}`.slice(-2); @@ -150,6 +164,8 @@ const formatActiveSubscription = (subscription: SubscriptionDetails, expiresAt: let status = ''; switch (subscription.action_type) { case 'free-trial': + status = 'active_trial'; + break; case 'recurrent': status = 'active'; break; @@ -166,6 +182,7 @@ const formatActiveSubscription = (subscription: SubscriptionDetails, expiresAt: return { subscriptionId: subscription.subscription_id, offerId: subscription.item_id?.toString(), + accessFeeId: `S${subscription.access_fee_id}`, status, expiresAt, nextPaymentAt: subscription.next_rebill_date, diff --git a/src/stores/AccountController.ts b/src/stores/AccountController.ts index 4f9b17dc8..d5df15082 100644 --- a/src/stores/AccountController.ts +++ b/src/stores/AccountController.ts @@ -442,26 +442,28 @@ export async function reloadActiveSubscription({ delay }: { delay: number } = { }); } -export async function exportAccountData() { +export const exportAccountData = async () => { return await useAccount(async () => { return await useService(async ({ accountService }) => { return await accountService.exportAccountData(undefined, true); }); }); -} +}; -export async function getSocialLoginUrls() { +export const getSocialLoginUrls = async () => { return await useService(async ({ accountService, config }) => { return await accountService.getSocialUrls(config); }); -} -export async function deleteAccountData(password: string) { +}; + +export const deleteAccountData = async (password: string) => { return await useAccount(async () => { return await useService(async ({ accountService }) => { return await accountService.deleteAccount({ password }, true); }); }); -} +}; + export const getReceipt = async (transactionId: string) => { return await useAccount(async () => { return await useService(async ({ subscriptionService, sandbox = true }) => { diff --git a/src/stores/CheckoutController.ts b/src/stores/CheckoutController.ts index 1cc918eb6..019fbb935 100644 --- a/src/stores/CheckoutController.ts +++ b/src/stores/CheckoutController.ts @@ -295,6 +295,16 @@ export const switchSubscription = async (toOfferId: string, switchDirection: 'up }); }; +export const changeSubscription = async ({ accessFeeId, subscriptionId }: { accessFeeId: string; subscriptionId: string }) => { + return await useService(async ({ subscriptionService, sandbox = true }) => { + if (!subscriptionService || !('changeSubscription' in subscriptionService)) throw new Error('subscription service is not configured'); + + const { responseData } = await subscriptionService.changeSubscription({ accessFeeId, subscriptionId }, sandbox); + + return responseData; + }); +}; + export const updatePayPalPaymentMethod = async ( successUrl: string, cancelUrl: string, diff --git a/src/stores/NotificationsController.ts b/src/stores/NotificationsController.ts index 7f2146821..23c23e476 100644 --- a/src/stores/NotificationsController.ts +++ b/src/stores/NotificationsController.ts @@ -26,7 +26,6 @@ export const subscribeToNotifications = async (uuid: string = '') => { break; case NotificationsTypes.ACCESS_GRANTED: await reloadActiveSubscription(); - window.location.href = addQueryParams(window.location.href, { u: 'welcome' }); break; case NotificationsTypes.ACCESS_REVOKED: await reloadActiveSubscription(); diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index f37511145..97b873e2d 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -110,8 +110,8 @@ export const buildLegacySeriesUrlFromMediaItem = (media: PlaylistItem, play: boo }); }; -export const formatPrice = (price: number, currency: string, country: string) => { - return new Intl.NumberFormat(country || undefined, { +export const formatPrice = (price: number, currency: string, country?: string) => { + return new Intl.NumberFormat(country || 'en-US', { style: 'currency', currency: currency, }).format(price); diff --git a/test-e2e/tests/payments/coupons_test.ts b/test-e2e/tests/payments/coupons_test.ts index babc4f67d..c05cbfbe6 100644 --- a/test-e2e/tests/payments/coupons_test.ts +++ b/test-e2e/tests/payments/coupons_test.ts @@ -14,6 +14,7 @@ const jwProps: ProviderProps = { locale: undefined, shouldMakePayment: true, canRenewSubscription: false, + hasInlineOfferSwitch: true, }; const cleengProps: ProviderProps = { @@ -26,6 +27,7 @@ const cleengProps: ProviderProps = { locale: 'NL', shouldMakePayment: false, canRenewSubscription: true, + hasInlineOfferSwitch: false, }; runTestSuite(jwProps, 'JW Player'); @@ -86,12 +88,12 @@ function runTestSuite(props: ProviderProps, providerName: string) { ); } - await finishAndCheckSubscription(I, addYear(today), today, props.yearlyOffer.price); + await finishAndCheckSubscription(I, addYear(today), today, props.yearlyOffer.price, props.hasInlineOfferSwitch); }); Scenario(`I can cancel a free subscription - ${providerName}`, async ({ I }) => { couponLoginContext = await I.registerOrLogin(couponLoginContext); - cancelPlan(I, addYear(today), props.canRenewSubscription); + cancelPlan(I, addYear(today), props.canRenewSubscription, providerName); }); Scenario(`I can renew a free subscription - ${providerName}`, async ({ I }) => { diff --git a/test-e2e/tests/payments/subscription_test.ts b/test-e2e/tests/payments/subscription_test.ts index 36f0a7c4d..3b3291ea8 100644 --- a/test-e2e/tests/payments/subscription_test.ts +++ b/test-e2e/tests/payments/subscription_test.ts @@ -13,6 +13,7 @@ const jwProps: ProviderProps = { applicableTax: 0, canRenewSubscription: false, fieldWrapper: '', + hasInlineOfferSwitch: true, }; const cleengProps: ProviderProps = { @@ -24,6 +25,7 @@ const cleengProps: ProviderProps = { applicableTax: 21, canRenewSubscription: true, fieldWrapper: 'iframe', + hasInlineOfferSwitch: false, }; runTestSuite(jwProps, 'JW Player'); @@ -170,7 +172,7 @@ function runTestSuite(props: ProviderProps, providerName: string) { props.fieldWrapper, ); - await finishAndCheckSubscription(I, addYear(today), today, props.yearlyOffer.price); + await finishAndCheckSubscription(I, addYear(today), today, props.yearlyOffer.price, props.hasInlineOfferSwitch); I.seeAll(cardInfo); }); @@ -178,7 +180,7 @@ function runTestSuite(props: ProviderProps, providerName: string) { Scenario(`I can cancel my subscription - ${providerName}`, async ({ I }) => { paidLoginContext = await I.registerOrLogin(paidLoginContext); - cancelPlan(I, addYear(today), props.canRenewSubscription); + cancelPlan(I, addYear(today), props.canRenewSubscription, providerName); // Still see payment info I.seeAll(cardInfo); diff --git a/test-e2e/utils/payments.ts b/test-e2e/utils/payments.ts index 13b14978f..8732da225 100644 --- a/test-e2e/utils/payments.ts +++ b/test-e2e/utils/payments.ts @@ -35,7 +35,7 @@ export function formatDate(date: Date) { return new Intl.DateTimeFormat('en-US', { day: 'numeric', month: 'long', year: 'numeric' }).format(date); } -export async function finishAndCheckSubscription(I: CodeceptJS.I, billingDate: Date, today: Date, yearlyPrice: string) { +export async function finishAndCheckSubscription(I: CodeceptJS.I, billingDate: Date, today: Date, yearlyPrice: string, hasInlineOfferSwitch: boolean) { I.click('Continue'); I.waitForLoaderDone(longTimeout); I.wait(2); @@ -63,18 +63,30 @@ export async function finishAndCheckSubscription(I: CodeceptJS.I, billingDate: D I.see(yearlyPrice); I.see('/year'); I.see('Next billing date is on ' + formatDate(billingDate)); - I.see('Cancel subscription'); - I.waitForElement('[class*="transactionItem"]'); + if (hasInlineOfferSwitch) { + I.waitForElement('[data-testid="change-subscription-button"]', 10); + I.click('[data-testid="change-subscription-button"]'); + } + + I.waitForElement('[data-testid="cancel-subscription-button"]', 10); + + I.waitForElement('[class*="transactionItem"]', 10); I.see(formatDate(today)); } -export function cancelPlan(I: CodeceptJS.I, expirationDate: Date, canRenewSubscription: boolean) { +export function cancelPlan(I: CodeceptJS.I, expirationDate: Date, canRenewSubscription: boolean, providerName?: string) { I.amOnPage(constants.paymentsUrl); I.waitForLoaderDone(); - I.click('Cancel subscription'); + if (providerName?.includes('JW')) { + I.waitForElement('[data-testid="change-subscription-button"]', 10); + I.click('[data-testid="change-subscription-button"]'); + } + + I.waitForElement('[data-testid="cancel-subscription-button"]', 10); + I.click('[data-testid="cancel-subscription-button"]'); I.see('We are sorry to see you go.'); I.see('You will be unsubscribed from your current plan by clicking the unsubscribe button below.'); I.see('Unsubscribe'); @@ -83,7 +95,8 @@ export function cancelPlan(I: CodeceptJS.I, expirationDate: Date, canRenewSubscr I.dontSee('This plan will expire'); - I.click('Cancel subscription'); + I.waitForElement('[data-testid="cancel-subscription-button"]', 10); + I.click('[data-testid="cancel-subscription-button"]'); I.click('Unsubscribe'); I.waitForLoaderDone(); I.see('Miss you already.'); @@ -122,7 +135,7 @@ export function renewPlan(I: CodeceptJS.I, billingDate: Date, yearlyPrice: strin I.see('Annual subscription'); I.see('Next billing date is on'); - I.see('Cancel subscription'); + I.waitForElement('[data-testid="cancel-subscription-button"]', 10); } export function overrideIP(I: CodeceptJS.I) { diff --git a/test/types.ts b/test/types.ts index 544188650..14532ef98 100644 --- a/test/types.ts +++ b/test/types.ts @@ -27,4 +27,5 @@ export type ProviderProps = { shouldMakePayment?: boolean; locale?: string | undefined; fieldWrapper?: string; + hasInlineOfferSwitch: boolean; }; diff --git a/types/account.d.ts b/types/account.d.ts index e03e5c119..f462205ff 100644 --- a/types/account.d.ts +++ b/types/account.d.ts @@ -312,6 +312,7 @@ export type UpdatePersonalShelvesArgs = { export type FirstLastNameInput = { firstName: string; lastName: string; + metadata?: Record; }; export type EmailConfirmPasswordInput = { @@ -343,5 +344,5 @@ type ChangePassword = EnvironmentServiceRequest>; type UpdatePersonalShelves = EnvironmentServiceRequest>; type GetLocales = EmptyServiceRequest; -type ExportAccountData = AuthServiceRequest; +type ExportAccountData = EnvironmentServiceRequest; type DeleteAccount = EnvironmentServiceRequest; diff --git a/types/checkout.d.ts b/types/checkout.d.ts index 79c6c9e82..777420b0e 100644 --- a/types/checkout.d.ts +++ b/types/checkout.d.ts @@ -41,6 +41,7 @@ export type Offer = { contentExternalId: number | null; contentExternalData: string | null; contentAgeRestriction: string | null; + planSwitchEnabled?: boolean; }; export type OrderOffer = { diff --git a/types/subscription.d.ts b/types/subscription.d.ts index 43908618a..9051255a4 100644 --- a/types/subscription.d.ts +++ b/types/subscription.d.ts @@ -1,9 +1,10 @@ -import type { DefaultCreditCardData, SetDefaultCard } from '@inplayer-org/inplayer.js'; +import type { DefaultCreditCardData, SetDefaultCard, ChangeSubscriptionPlanRequestBody, ChangeSubscriptionPlanResponse } from '@inplayer-org/inplayer.js'; // Subscription types export type Subscription = { subscriptionId: number | string; offerId: string; - status: 'active' | 'cancelled' | 'expired' | 'terminated'; + accessFeeId?: string; + status: 'active' | 'active_trial' | 'cancelled' | 'expired' | 'terminated'; expiresAt: number; nextPaymentPrice: number; nextPaymentCurrency: string; @@ -122,8 +123,15 @@ export type FetchReceiptPayload = { export type FetchReceiptResponse = string; +type ChangeSubscriptionPayload = { + accessFeeId: string; + subscriptionId: string; +}; + type GetSubscriptions = CleengRequest; type UpdateSubscription = CleengRequest; type GetPaymentDetails = CleengRequest; type GetTransactions = CleengRequest; type FetchReceipt = CleengRequest; + +type ChangeSubscription = EnvironmentServiceRequest; diff --git a/yarn.lock b/yarn.lock index b73d83327..d4acbe708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1732,10 +1732,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@inplayer-org/inplayer.js@^3.13.12": - version "3.13.12" - resolved "https://registry.yarnpkg.com/@inplayer-org/inplayer.js/-/inplayer.js-3.13.12.tgz#6a8ff6516ff92fd1a38ef4e2854de512c29ad44b" - integrity sha512-NYmpZINBxu/vR4eTFtT84NfN0TcHH1Tplf+j36lQeiSa6Xj0mN6BlTUpZYXVseuOb9SePvGUbPyjXBCIAhALrg== +"@inplayer-org/inplayer.js@^3.13.13": + version "3.13.13" + resolved "https://registry.yarnpkg.com/@inplayer-org/inplayer.js/-/inplayer.js-3.13.13.tgz#b64dfee35e488037f64608a8264aecc6e85252da" + integrity sha512-LnyriDdasvRy1CUv2Zg0THRs48s+LffhnHO7hEf4K7gOl5DaFmkf16OM1uOBuOjL4xzHGgFR3IfPz7Pay3uzsA== dependencies: aws-iot-device-sdk "^2.2.6" axios "^0.21.2" @@ -3576,7 +3576,7 @@ compare-func@^2.0.0: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== concat-stream@^2.0.0, concat-stream@~2.0.0: version "2.0.0" @@ -3683,9 +3683,9 @@ core-js-pure@^3.25.1, core-js-pure@^3.25.3: integrity sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg== core-js@^3.19.2: - version "3.22.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.3.tgz#498c41d997654cb00e81c7a54b44f0ab21ab01d5" - integrity sha512-1t+2a/d2lppW1gkLXx3pKPVGbBdxXAkqztvWb1EJ8oF8O2gIGiytzflNiFEehYwVK/t2ryUsGBoOFFvNx95mbg== + version "3.31.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.0.tgz#4471dd33e366c79d8c0977ed2d940821719db344" + integrity sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ== core-util-is@~1.0.0: version "1.0.3" @@ -3897,14 +3897,14 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.2.0, debug@^4.3.1: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.2, debug@^4.3.4: +debug@4.3.4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4981,7 +4981,7 @@ fs-tree-diff@^2.0.1: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: version "2.3.2" @@ -5013,7 +5013,17 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + +get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -5094,7 +5104,7 @@ glob-stream@^6.1.0: to-absolute-glob "^2.0.0" unique-stream "^2.0.2" -glob@7.2.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@7.2.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -5117,6 +5127,18 @@ glob@^6.0.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -5226,7 +5248,12 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1, has-symbols@^1.0.2: +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -5507,7 +5534,7 @@ indent-string@^4.0.0: inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -5857,7 +5884,7 @@ is-wsl@^2.2.0: isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" @@ -6645,7 +6672,7 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -"minimatch@2 || 3", minimatch@5.0.1, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@5.0.1, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6673,12 +6700,12 @@ minimist@1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -minimist@^1.1.0: +minimist@^1.1.0, minimist@^1.2.5: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== @@ -6988,11 +7015,16 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.11.0, object-inspect@^1.9.0: +object-inspect@^1.11.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -7051,7 +7083,7 @@ oblivious-set@1.0.0: once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -7271,7 +7303,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^2.0.1: version "2.0.1" @@ -7971,7 +8003,20 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.3.3: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7984,7 +8029,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -9326,7 +9371,7 @@ typedarray-to-buffer@^3.1.5: typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== typescript@^4.2.4, typescript@^4.3.4: version "4.6.2" @@ -9481,7 +9526,7 @@ use-debounce@^7.0.1: util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== uuid@9.0.0, uuid@^9.0: version "9.0.0" @@ -10062,7 +10107,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: version "3.0.3"