From efc334f834ad6ae520ac3832994b7c5236d842c2 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Tue, 11 Jun 2024 13:44:21 +0200 Subject: [PATCH 01/44] fix: add support for multiple plan offers and prices grouped by access period --- .../src/controllers/AccountController.ts | 1 + packages/common/src/env.ts | 2 +- packages/common/src/modules/register.ts | 2 + .../services/integrations/CheckoutService.ts | 3 ++ .../cleeng/CleengCheckoutService.ts | 2 + .../integrations/jwp/JWPAccountService.ts | 12 ++--- .../integrations/jwp/JWPCheckoutService.ts | 52 +++++++++++++------ .../integrations/jwp/base/JWPBaseService.ts | 51 ++++++++++++++++++ packages/common/src/stores/ConfigStore.ts | 1 + packages/common/src/utils/configSchema.ts | 1 + packages/common/types/config.ts | 1 + packages/common/types/jw.ts | 30 +++++++++++ .../src/components/Button/Button.module.scss | 28 ++++++++++ .../src/components/Button/ButtonGroup.tsx | 17 ++++++ .../ChooseOfferForm.module.scss | 13 ++++- .../ChooseOfferForm/ChooseOfferForm.tsx | 28 +++++++--- .../src/components/Dialog/Dialog.module.scss | 2 +- .../AccountModal/forms/ChooseOffer.tsx | 13 +++-- platforms/web/.env | 2 +- platforms/web/public/locales/en/account.json | 2 +- platforms/web/public/locales/es/account.json | 2 +- .../tests/offers/choose_offer_test.ts | 3 +- yarn.lock | 33 ++---------- 23 files changed, 231 insertions(+), 70 deletions(-) create mode 100644 packages/common/src/services/integrations/jwp/base/JWPBaseService.ts create mode 100644 packages/common/types/jw.ts create mode 100644 packages/ui-react/src/components/Button/ButtonGroup.tsx diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index af9cd99ee..86af7cfb5 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -86,6 +86,7 @@ export default class AccountController { await this.profileController?.loadPersistedProfile(); await this.accountService.initialize(config, url, this.logout); + await this.checkoutService.initialize(config); // set the accessModel before restoring the user session useConfigStore.setState({ accessModel: this.accountService.accessModel }); diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index 50c854c56..7ce25d4c8 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -14,7 +14,7 @@ export type Env = { const env: Env = { APP_VERSION: '', - APP_API_BASE_URL: 'https://cdn.jwplayer.com', + APP_API_BASE_URL: 'https://content-portal.jwplatform.com', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', APP_DEFAULT_LANGUAGE: 'en', diff --git a/packages/common/src/modules/register.ts b/packages/common/src/modules/register.ts index e26533416..ed61861f2 100644 --- a/packages/common/src/modules/register.ts +++ b/packages/common/src/modules/register.ts @@ -45,6 +45,7 @@ import JWPSubscriptionService from '../services/integrations/jwp/JWPSubscription import JWPProfileService from '../services/integrations/jwp/JWPProfileService'; import { getIntegrationType } from './functions/getIntegrationType'; import { isCleengIntegrationType, isJwpIntegrationType } from './functions/calculateIntegrationType'; +import JWPBaseService from '../services/integrations/jwp/base/JWPBaseService'; // Common services container.bind(ConfigService).toSelf(); @@ -83,6 +84,7 @@ container.bind(SubscriptionService).to(CleengSubscriptionService).whenTargetName container.bind(DETERMINE_INTEGRATION_TYPE).toConstantValue(isJwpIntegrationType); container.bind(JWPEntitlementService).toSelf(); container.bind(AccountService).to(JWPAccountService).whenTargetNamed(INTEGRATION.JWP); +container.bind(JWPBaseService).toSelf(); container.bind(CheckoutService).to(JWPCheckoutService).whenTargetNamed(INTEGRATION.JWP); container.bind(SubscriptionService).to(JWPSubscriptionService).whenTargetNamed(INTEGRATION.JWP); container.bind(ProfileService).to(JWPProfileService).whenTargetNamed(INTEGRATION.JWP); diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index 483422073..8006e55b3 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -20,8 +20,11 @@ import type { UpdateOrder, UpdatePaymentWithPayPal, } from '../../../types/checkout'; +import type { Config } from '../../../types/config'; export default abstract class CheckoutService { + abstract initialize: (config: Config) => Promise; + abstract getOffers: GetOffers; abstract createOrder: CreateOrder; diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index 6aa89be0e..3c81816a7 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -41,6 +41,8 @@ export default class CleengCheckoutService extends CheckoutService { this.getCustomerIP = getCustomerIP; } + initialize = async () => {}; + getOffers: GetOffers = async (payload) => { return await Promise.all( payload.offerIds.map(async (offerId) => { diff --git a/packages/common/src/services/integrations/jwp/JWPAccountService.ts b/packages/common/src/services/integrations/jwp/JWPAccountService.ts index 5daba8d5e..ad1fd52a5 100644 --- a/packages/common/src/services/integrations/jwp/JWPAccountService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAccountService.ts @@ -135,18 +135,14 @@ export default class JWPAccountService extends AccountService { // set environment this.sandbox = !!jwpConfig.useSandbox; - const env: string = this.sandbox ? InPlayerEnv.Development : InPlayerEnv.Production; + const env: string = this.sandbox ? InPlayerEnv.Daily : InPlayerEnv.Production; InPlayer.setConfig(env as Env); // calculate access model - if (jwpConfig.clientId) { - this.clientId = jwpConfig.clientId; - } + this.clientId = jwpConfig.clientId; - if (jwpConfig.assetId) { - this.accessModel = ACCESS_MODEL.SVOD; - this.assetId = jwpConfig.assetId; - this.svodOfferIds = jwpConfig.assetId ? [String(jwpConfig.assetId)] : []; + if (jwpConfig.planId) { + this.svodOfferIds = jwpConfig.planId ? jwpConfig.planId.split(',') : []; } // restore session from URL params diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 678c50cd5..f6c67056a 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -1,6 +1,7 @@ -import InPlayer, { type AccessFee, type MerchantPaymentMethod } from '@inplayer-org/inplayer.js'; -import { injectable } from 'inversify'; +import InPlayer, { type MerchantPaymentMethod } from '@inplayer-org/inplayer.js'; +import { inject, injectable } from 'inversify'; +import { type JwPlanPricesResponse, type PlanPrice } from '../../../../../../packages/common/types/jw'; import { isSVODOffer } from '../../../utils/offers'; import type { CardPaymentData, @@ -19,14 +20,30 @@ import type { PaymentWithPayPal, UpdateOrder, } from '../../../../types/checkout'; +import type { Config } from '../../../../types/config'; import CheckoutService from '../CheckoutService'; import type { ServiceResponse } from '../../../../types/service'; import { isCommonError } from '../../../utils/api'; +import AccountService from '../AccountService'; +import { INTEGRATION_TYPE } from '../../../modules/types'; +import { getNamedModule } from '../../../modules/container'; + +import JWPBaseService from './base/JWPBaseService'; @injectable() export default class JWPCheckoutService extends CheckoutService { + private readonly jwpService: JWPBaseService; + private readonly accountService: AccountService; private readonly cardPaymentProvider = 'stripe'; + constructor(jwpService: JWPBaseService, @inject(INTEGRATION_TYPE) integrationType: string) { + super(); + this.jwpService = jwpService; + this.accountService = getNamedModule(AccountService, integrationType); + } + + initialize = async (config: Config) => this.jwpService.setup(this.accountService.sandbox, config.siteId); + private formatPaymentMethod = (method: MerchantPaymentMethod, cardPaymentProvider: string): PaymentMethod => { return { id: method.id, @@ -46,22 +63,21 @@ export default class JWPCheckoutService extends CheckoutService { }; }; - private formatOffer = (offer: AccessFee): Offer => { - const ppvOffers = ['ppv', 'ppv_custom']; - const offerId = ppvOffers.includes(offer.access_type.name) ? `C${offer.id}` : `S${offer.id}`; + private formatOffer = (offer: PlanPrice): Offer => { + const offerId = offer.access.type === 'subscription' ? `S${offer.id}` : `C${offer.id}`; return { id: offer.id, offerId, - offerCurrency: offer.currency, - customerPriceInclTax: offer.amount, - customerCurrency: offer.currency, - offerTitle: offer.description, + offerCurrency: offer.metadata.currency, + customerPriceInclTax: offer.metadata.amount, + customerCurrency: offer.metadata.currency, + offerTitle: offer.metadata.name, 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; + period: offer.access.period, + freePeriods: offer.metadata.trial?.quantity ?? 0, + planSwitchEnabled: false, + } as unknown as Offer; }; private formatOrder = (payload: CreateOrderArgs): Order => { @@ -97,11 +113,15 @@ export default class JWPCheckoutService extends CheckoutService { getOffers: GetOffers = async (payload) => { const offers = await Promise.all( - payload.offerIds.map(async (assetId) => { + payload.offerIds.map(async (planId) => { try { - const { data } = await InPlayer.Asset.getAssetAccessFees(parseInt(`${assetId}`)); + const data = await this.jwpService.get(`/plans/${planId}/prices`); + + if (data.prices) { + return data.prices.map((offer) => this.formatOffer(offer)); + } - return data?.map((offer) => this.formatOffer(offer)); + return []; } catch { throw new Error('Failed to get offers'); } diff --git a/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts b/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts new file mode 100644 index 000000000..3e5624d0e --- /dev/null +++ b/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts @@ -0,0 +1,51 @@ +import { injectable } from 'inversify'; + +type RequestOptions = { + authenticate?: boolean; + keepalive?: boolean; +}; + +@injectable() +export default class JWPBaseService { + private sandbox = true; + + private siteId = ''; + + // private getBaseUrl = () => (this.sandbox ? 'https://staging-v2.inplayer.com' : 'https://services.inplayer.com'); + private getBaseUrl = () => `${this.sandbox ? 'https://services-daily.inplayer.com' : 'https://services.inplayer.com'}/v3/sites/${this.siteId}`; + + private performRequest = async (path: string = '/', method = 'GET', body?: string, options: RequestOptions = {}) => { + try { + const resp = await fetch(`${this.getBaseUrl()}${path}`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + keepalive: options.keepalive, + method, + body, + }); + + return await resp.json(); + } catch (error: unknown) { + return { + errors: Array.of(error instanceof Error ? error.message : String(error)), + }; + } + }; + + setup = (sandbox: boolean, siteId: string) => { + this.sandbox = sandbox; + this.siteId = siteId; + }; + + get = (path: string, options?: RequestOptions) => this.performRequest(path, 'GET', undefined, options) as Promise; + + patch = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'PATCH', body, options) as Promise; + + put = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'PUT', body, options) as Promise; + + post = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'POST', body, options) as Promise; + + remove = (path: string, options?: RequestOptions) => this.performRequest(path, 'DELETE', undefined, options) as Promise; +} diff --git a/packages/common/src/stores/ConfigStore.ts b/packages/common/src/stores/ConfigStore.ts index 4a71a4115..816ae6f1e 100644 --- a/packages/common/src/stores/ConfigStore.ts +++ b/packages/common/src/stores/ConfigStore.ts @@ -32,6 +32,7 @@ export const useConfigStore = createStore('ConfigStore', () => ({ jwp: { clientId: null, assetId: null, + planId: null, useSandbox: true, }, }, diff --git a/packages/common/src/utils/configSchema.ts b/packages/common/src/utils/configSchema.ts index e564a4eac..dbaa4469d 100644 --- a/packages/common/src/utils/configSchema.ts +++ b/packages/common/src/utils/configSchema.ts @@ -34,6 +34,7 @@ const cleengSchema: SchemaOf = object({ const jwpSchema: SchemaOf = object({ clientId: string().nullable(), assetId: number().nullable(), + planId: string().nullable(), useSandbox: boolean().default(true), }); diff --git a/packages/common/types/config.ts b/packages/common/types/config.ts index fa1a790f2..a1387f6b5 100644 --- a/packages/common/types/config.ts +++ b/packages/common/types/config.ts @@ -76,6 +76,7 @@ export type JWP = { clientId?: string | null; assetId?: number | null; useSandbox?: boolean; + planId?: string | null; }; export type Features = { recommendationsPlaylist?: string | null; diff --git a/packages/common/types/jw.ts b/packages/common/types/jw.ts new file mode 100644 index 000000000..7c7c0cdd8 --- /dev/null +++ b/packages/common/types/jw.ts @@ -0,0 +1,30 @@ +export type JwListResponse = Record> = { + page: number; + total: number; + page_length: number; +} & { + [P in N]: T[]; +}; + +export type PlanPrice = { + id: string; + access: { + period: 'month' | 'year'; + quantity: number; + type: 'subscription'; + }; + metadata: { + amount: number; + currency: string; + name: string; + trial?: { period: 'day'; quantity: number } | null; + }; + original_id: number; + relationships: { + plans: { id: string; type: 'plan' }[]; + }; + schema: string; + type: 'price'; +}; + +export type JwPlanPricesResponse = JwListResponse<'prices', PlanPrice>; diff --git a/packages/ui-react/src/components/Button/Button.module.scss b/packages/ui-react/src/components/Button/Button.module.scss index b8e5fcfe3..fda3dbbf5 100644 --- a/packages/ui-react/src/components/Button/Button.module.scss +++ b/packages/ui-react/src/components/Button/Button.module.scss @@ -154,3 +154,31 @@ $large-button-height: 40px; margin: auto; transform: translate(-5px, -5px); } + +.buttonGroup { + .button { + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:not(:first-child):not(:last-child) { + border-right: none; + border-left: none; + } + + &:not(.disabled) { + &:hover, + &:focus { + z-index: 1; + transform: none; + } + } + } +} + diff --git a/packages/ui-react/src/components/Button/ButtonGroup.tsx b/packages/ui-react/src/components/Button/ButtonGroup.tsx new file mode 100644 index 000000000..dca081720 --- /dev/null +++ b/packages/ui-react/src/components/Button/ButtonGroup.tsx @@ -0,0 +1,17 @@ +import type { FC, ReactNode } from 'react'; +import classNames from 'classnames'; + +import styles from './Button.module.scss'; + +type ButtonGroupProps = { + children?: ReactNode; + className?: string; +} & React.HTMLAttributes; + +const ButtonGroup: FC = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export default ButtonGroup; diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss index f9bcbe936..a7b4288c4 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss @@ -4,7 +4,7 @@ @use '@jwp/ott-ui-react/src/styles/mixins/responsive'; .title { - margin-bottom: 8px; + margin-bottom: 16px; font-weight: var(--body-font-weight-bold); font-size: 24px; } @@ -15,6 +15,12 @@ font-size: 18px; } +.tabs { + display: flex; + justify-content: center; + margin-bottom: 16px; +} + .offerGroupSwitch { display: flex; flex: 1; @@ -31,11 +37,16 @@ .offers { display: flex; + justify-content: space-around; margin: 0 -4px 24px; + padding-bottom: 8px; + overflow-x: auto; } .offer { flex: 1; + min-width: 200px; + max-width: 250px; margin: 0 4px; &:focus-within .label { diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx index 0418f39f5..1cb06148e 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx @@ -1,14 +1,14 @@ -import React, { type FC, type SVGProps } from 'react'; +import React, { useState, type FC, type SVGProps, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import type { FormErrors } from '@jwp/ott-common/types/form'; import type { Offer, ChooseOfferFormData } from '@jwp/ott-common/types/checkout'; -import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { getOfferPrice, isSVODOffer } from '@jwp/ott-common/src/utils/offers'; import { testId } from '@jwp/ott-common/src/utils/common'; import CheckCircle from '@jwp/ott-theme/assets/icons/check_circle.svg?react'; import Button from '../Button/Button'; +import ButtonGroup from '../Button/ButtonGroup'; import FormFeedback from '../FormFeedback/FormFeedback'; import DialogBackButton from '../DialogBackButton/DialogBackButton'; import LoadingOverlay from '../LoadingOverlay/LoadingOverlay'; @@ -85,7 +85,7 @@ const OfferBox: React.FC = ({ offer, selected, onChange }: OfferB const isMonthly = offer.period === 'month'; return renderOption({ - title: isMonthly ? t('choose_offer.monthly') : t('choose_offer.yearly'), + title: offer.offerTitle, secondBenefit: t('choose_offer.benefits.cancel_anytime'), periodString: isMonthly ? t('periods.month') : t('periods.year'), }); @@ -100,6 +100,8 @@ const OfferBox: React.FC = ({ offer, selected, onChange }: OfferB }); }; +type OfferPeriod = 'month' | 'year'; + type Props = { values: ChooseOfferFormData; errors: FormErrors; @@ -112,16 +114,28 @@ type Props = { }; const ChooseOfferForm: React.FC = ({ values, errors, submitting, offers, showOfferTypeSwitch, onChange, onSubmit, onBackButtonClickHandler }: Props) => { - const siteName = useConfigStore((s) => s.config.siteName); const { t } = useTranslation('account'); const { selectedOfferType, selectedOfferId } = values; + const groupedOffers = useMemo( + () => offers.reduce((acc, offer) => ({ ...acc, [offer.period]: [...(acc[offer.period as OfferPeriod] || []), offer] }), {} as Record), + [offers], + ); + + const [offerFilter, setOfferFilter] = useState(() => Object.keys(groupedOffers)[0] as OfferPeriod); + return (
{onBackButtonClickHandler ? : null}

{t('choose_offer.title')}

-

{t('choose_offer.watch_this_on_platform', { siteName })}

{errors.form ? {errors.form} : null} +
+ + {Object.keys(groupedOffers).map((period) => ( +
{showOfferTypeSwitch && (
= ({ values, errors, submitting, offers, {!offers.length ? (

{t('choose_offer.no_pricing_available')}

) : ( - offers.map((offer) => ) + groupedOffers[offerFilter].map((offer) => ( + + )) )}
{submitting && } diff --git a/packages/ui-react/src/components/Dialog/Dialog.module.scss b/packages/ui-react/src/components/Dialog/Dialog.module.scss index 236a6d014..601e784a3 100644 --- a/packages/ui-react/src/components/Dialog/Dialog.module.scss +++ b/packages/ui-react/src/components/Dialog/Dialog.module.scss @@ -19,7 +19,7 @@ } .small { - max-width: 450px; + max-width: 665px; } .large { diff --git a/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx b/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx index e1730074e..3b4807656 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx @@ -1,8 +1,8 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { mixed, object } from 'yup'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router'; -import type { ChooseOfferFormData, OfferType } from '@jwp/ott-common/types/checkout'; +import type { ChooseOfferFormData, Offer, OfferType } from '@jwp/ott-common/types/checkout'; import { modalURLFromLocation } from '@jwp/ott-ui-react/src/utils/location'; import useOffers from '@jwp/ott-hooks-react/src/useOffers'; import useForm from '@jwp/ott-hooks-react/src/useForm'; @@ -57,6 +57,8 @@ const ChooseOffer = () => { const visibleOffers = values.selectedOfferType === 'tvod' ? mediaOffers : isSwitch ? switchSubscriptionOffers : subscriptionOffers; + const offersRef = useRef([]); + useEffect(() => { if (isLoading || !visibleOffers.length) return; @@ -71,8 +73,13 @@ const ChooseOffer = () => { setValue('selectedOfferType', defaultOfferType); }, [isLoading, defaultOfferType, setValue]); + useLayoutEffect(() => { + offersRef.current = defaultOfferType === 'tvod' ? mediaOffers : isSwitch ? switchSubscriptionOffers : subscriptionOffers; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultOfferType]); + // loading state - if (isLoading) { + if (isLoading || offersRef.current !== visibleOffers) { return (
diff --git a/platforms/web/.env b/platforms/web/.env index fb9ab08f5..6d64bfabd 100644 --- a/platforms/web/.env +++ b/platforms/web/.env @@ -1,4 +1,4 @@ -APP_API_BASE_URL=https://cdn.jwplayer.com +APP_API_BASE_URL=https://content-portal.jwplatform.com APP_PLAYER_ID=M4qoGvUk # page metadata (SEO) diff --git a/platforms/web/public/locales/en/account.json b/platforms/web/public/locales/en/account.json index 5ca046894..264b4061a 100644 --- a/platforms/web/public/locales/en/account.json +++ b/platforms/web/public/locales/en/account.json @@ -70,7 +70,7 @@ "no_pricing_available": "There are no pricing options available at the moment.", "one_time_only": "This video", "subscription": "Subscription", - "title": "Choose plan", + "title": "Choose your plan", "tvod_access_one": "{{count}} {{period}} access", "tvod_access_other": "{{count}} {{period}} access", "watch_this_on_platform": "Watch this on {{siteName}}", diff --git a/platforms/web/public/locales/es/account.json b/platforms/web/public/locales/es/account.json index e0bddc27f..63b236f02 100644 --- a/platforms/web/public/locales/es/account.json +++ b/platforms/web/public/locales/es/account.json @@ -75,7 +75,7 @@ "no_pricing_available": "No hay opciones de precios disponibles en este momento.", "one_time_only": "Este video", "subscription": "Suscripción", - "title": "Elige un plan", + "title": "Elige tu plan", "tvod_access_one": "Acceso de {{count}} {{period}}", "tvod_access_many": "Acceso de {{count}} {{period}}", "tvod_access_other": "Acceso de {{count}} {{period}}", diff --git a/platforms/web/test-e2e/tests/offers/choose_offer_test.ts b/platforms/web/test-e2e/tests/offers/choose_offer_test.ts index 5a7b4fb52..48c472192 100644 --- a/platforms/web/test-e2e/tests/offers/choose_offer_test.ts +++ b/platforms/web/test-e2e/tests/offers/choose_offer_test.ts @@ -67,8 +67,7 @@ function runTestSuite(props: ProviderProps, providerName: string) { I.click('Complete subscription'); I.waitForLoaderDone(); - I.see('Choose plan'); - I.see('Watch this on JW OTT Web App'); + I.see('Choose your plan'); await within(props.monthlyOffer.label, () => { I.see('Monthly'); diff --git a/yarn.lock b/yarn.lock index 8bbaeb095..e70e3d07d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,7 +1879,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": +"@jridgewell/set-array@^1.0.1", "@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== @@ -9930,7 +9930,7 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9947,15 +9947,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10053,7 +10044,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10074,13 +10065,6 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -11694,16 +11678,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From f9246eb9846f8750f423549abf349e7e3a84422c Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Tue, 11 Jun 2024 13:50:03 +0200 Subject: [PATCH 02/44] fix: update snaps --- .../ChooseOfferForm.test.tsx.snap | 155 +++++------------- 1 file changed, 39 insertions(+), 116 deletions(-) diff --git a/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap b/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap index 92198e490..310fb7983 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap +++ b/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap @@ -11,11 +11,30 @@ exports[` > renders and matches snapshot 1`] = ` > choose_offer.title -

- choose_offer.watch_this_on_platform -

+
+ + +
+
@@ -73,7 +92,7 @@ exports[` > renders and matches snapshot 1`] = ` class="_offerTitle_f7961f" id="title-S916977979_NL" > - choose_offer.monthly + Monthly subscription (recurring) to JW OTT Webapp
-
- -
-
+ +
From b3c3141e0278e750e40f96b8e3b42954b773ddee Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Tue, 11 Jun 2024 21:39:57 +0200 Subject: [PATCH 03/44] fix: replace any with unknown --- packages/common/types/jw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/types/jw.ts b/packages/common/types/jw.ts index 7c7c0cdd8..e642912ca 100644 --- a/packages/common/types/jw.ts +++ b/packages/common/types/jw.ts @@ -1,4 +1,4 @@ -export type JwListResponse = Record> = { +export type JwListResponse = Record> = { page: number; total: number; page_length: number; From 2ed2d7ff21e11acdc9d35a7f4fa65c9922eacd27 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Wed, 12 Jun 2024 09:44:20 +0200 Subject: [PATCH 04/44] fix: adjust modal size --- packages/ui-react/src/components/Dialog/Dialog.module.scss | 4 ++++ packages/ui-react/src/components/Dialog/Dialog.tsx | 2 +- .../ui-react/src/containers/AccountModal/AccountModal.tsx | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/ui-react/src/components/Dialog/Dialog.module.scss b/packages/ui-react/src/components/Dialog/Dialog.module.scss index 601e784a3..e23db78b9 100644 --- a/packages/ui-react/src/components/Dialog/Dialog.module.scss +++ b/packages/ui-react/src/components/Dialog/Dialog.module.scss @@ -19,6 +19,10 @@ } .small { + max-width: 450px; +} + +.medium { max-width: 665px; } diff --git a/packages/ui-react/src/components/Dialog/Dialog.tsx b/packages/ui-react/src/components/Dialog/Dialog.tsx index 9bd933f7e..a16f68ff5 100644 --- a/packages/ui-react/src/components/Dialog/Dialog.tsx +++ b/packages/ui-react/src/components/Dialog/Dialog.tsx @@ -11,7 +11,7 @@ import styles from './Dialog.module.scss'; type Props = { open: boolean; onClose: () => void; - size?: 'small' | 'large'; + size?: 'small' | 'medium' | 'large'; children: React.ReactNode; role?: React.AriaRole; } & React.AriaAttributes; diff --git a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx index 68e4ccfd4..da147cc26 100644 --- a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx +++ b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx @@ -163,7 +163,7 @@ const AccountModal = () => { }; const shouldShowBanner = !['delete-account', 'delete-account-confirmation', 'edit-card', 'warning-account-deletion'].includes(view ?? ''); - const dialogSize = ['delete-account-confirmation'].includes(view ?? '') ? 'large' : 'small'; + const dialogSize = ['delete-account-confirmation'].includes(view ?? '') ? 'large' : ['choose-offer'].includes(view ?? '') ? 'medium' : 'small'; return ( From ce9f1a9b48bdef0b8f94c1d57243da9a9f8ec3a5 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Fri, 7 Jun 2024 12:14:47 +0200 Subject: [PATCH 05/44] fix(payment): waiting for payment not working for jwp ppv --- .../src/controllers/AccountController.ts | 2 +- .../src/controllers/CheckoutController.ts | 8 +++----- .../integrations/jwp/JWPCheckoutService.ts | 20 ++++++++++++++++++- packages/hooks-react/src/useCheckAccess.ts | 18 ++++++++++------- .../WaitingForPayment/WaitingForPayment.tsx | 11 ++++++++-- platforms/web/test-e2e/utils/constants.ts | 4 ++-- 6 files changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index 86af7cfb5..60498f6da 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -420,7 +420,7 @@ export default class AccountController { let pendingOffer: Offer | null = null; if (!activeSubscription && !!retry && retry > 0) { - const retryDelay = 1500; // Any initial delay has already occured, so we can set this to a fixed value + const retryDelay = 1500; // Any initial delay has already occurred, so we can set this to a fixed value return await this.reloadSubscriptions({ delay: retryDelay, retry: retry - 1 }); } diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 917699af9..3695e4af9 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -242,13 +242,11 @@ export default class CheckoutController { const { getAccountInfo } = useAccountStore.getState(); const { customerId } = getAccountInfo(); - - assertModuleMethod(this.checkoutService.getSubscriptionSwitches, 'getSubscriptionSwitches is not available in checkout service'); - assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service'); - const { subscription } = useAccountStore.getState(); - if (!subscription) return null; + if (!subscription || !this.checkoutService.getSubscriptionSwitches) return null; + + assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service'); const response = await this.checkoutService.getSubscriptionSwitches({ customerId: customerId, diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index f6c67056a..6289f297b 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -63,6 +63,24 @@ export default class JWPCheckoutService extends CheckoutService { }; }; + /** + * Parse the given offer id and extract the asset id. + * The offer id might be the Cleeng format (`S_`) or the asset id as string. + */ + private parseOfferId(offerId: string | number) { + if (typeof offerId === 'string') { + // offer id format `S_` + if (offerId.startsWith('C') || offerId.startsWith('S')) { + return parseInt(offerId.slice(1).split('_')[0]); + } + + // offer id format `` + return parseInt(offerId); + } + + return offerId; + } + private formatOffer = (offer: PlanPrice): Offer => { const offerId = offer.access.type === 'subscription' ? `S${offer.id}` : `C${offer.id}`; @@ -237,7 +255,7 @@ export default class JWPCheckoutService extends CheckoutService { getEntitlements: GetEntitlements = async ({ offerId }) => { try { - const response = await InPlayer.Asset.checkAccessForAsset(parseInt(offerId)); + const response = await InPlayer.Asset.checkAccessForAsset(this.parseOfferId(offerId)); return this.formatEntitlements(response.data.expires_at, true); } catch { return this.formatEntitlements(); diff --git a/packages/hooks-react/src/useCheckAccess.ts b/packages/hooks-react/src/useCheckAccess.ts index de50537e4..701e9330d 100644 --- a/packages/hooks-react/src/useCheckAccess.ts +++ b/packages/hooks-react/src/useCheckAccess.ts @@ -8,7 +8,7 @@ type IntervalCheckAccessPayload = { interval?: number; iterations?: number; offerId?: string; - callback?: (hasAccess: boolean) => void; + callback?: ({ hasAccess, offerId }: { hasAccess: boolean; offerId: string }) => void; }; const useCheckAccess = () => { @@ -22,21 +22,25 @@ const useCheckAccess = () => { const offers = checkoutController.getSubscriptionOfferIds(); const intervalCheckAccess = useCallback( - ({ interval = 3000, iterations = 5, offerId, callback }: IntervalCheckAccessPayload) => { - if (!offerId && offers?.[0]) { - offerId = offers[0]; + ({ interval = 3000, iterations = 5, offerId = offers?.[0], callback }: IntervalCheckAccessPayload) => { + if (!offerId) { + callback?.({ hasAccess: false, offerId: '' }); + return; } intervalRef.current = window.setInterval(async () => { const hasAccess = await accountController.checkEntitlements(offerId); if (hasAccess) { - await accountController.reloadSubscriptions({ retry: 10, delay: 2000 }); - callback?.(true); + window.clearInterval(intervalRef.current); + // No duplicate retry mechanism. This can also be a TVOD offer which isn't validated using the + // reloadSubscriptions method. + await accountController.reloadSubscriptions(); + callback?.({ hasAccess: true, offerId: offerId || '' }); } else if (--iterations === 0) { window.clearInterval(intervalRef.current); setErrorMessage(t('payment.longer_than_usual')); - callback?.(false); + callback?.({ hasAccess: false, offerId: offerId || '' }); } }, interval); }, diff --git a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx index 9e40b7319..a11b4820a 100644 --- a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx +++ b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx @@ -23,11 +23,18 @@ const WaitingForPayment = () => { interval: 3000, iterations: 5, offerId, - callback: (hasAccess) => { + callback: ({ hasAccess, offerId }) => { if (!hasAccess) return; announce(t('checkout.payment_success'), 'success'); - navigate(modalURLFromLocation(location, 'welcome')); + + // close the modal for PPV/TVOD offers + if (offerId.startsWith('C') || offerId.startsWith('P')) { + // @TODO should we show a dedicated modal for TVOD access? + navigate(modalURLFromLocation(location, null)); + } else { + navigate(modalURLFromLocation(location, 'welcome')); + } }, }); //eslint-disable-next-line diff --git a/platforms/web/test-e2e/utils/constants.ts b/platforms/web/test-e2e/utils/constants.ts index fa0f4be5a..fbc18d5c9 100644 --- a/platforms/web/test-e2e/utils/constants.ts +++ b/platforms/web/test-e2e/utils/constants.ts @@ -81,7 +81,7 @@ export default { paymentFee: formatPrice(0, 'EUR', 'NL'), }, inplayer: { - label: `label[for="S38279"]`, + label: `label[for="S118699_38279"]`, price: formatPrice(6.99, 'EUR'), paymentFee: formatPrice(0, 'EUR'), }, @@ -93,7 +93,7 @@ export default { paymentFee: formatPrice(0, 'EUR', 'NL'), }, inplayer: { - label: `label[for="S38280"]`, + label: `label[for="S118699_38280"]`, price: formatPrice(50, 'EUR'), paymentFee: formatPrice(0, 'EUR'), }, From 5a8f0bd4eeab439ba3029cc6413b07ae32e02e2d Mon Sep 17 00:00:00 2001 From: borkopetrovicc <104987342+borkopetrovicc@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:13:10 +0200 Subject: [PATCH 06/44] Remove Profiles feature from WebApp (#543) * fix: remove profiles * fix: remove code * fix: remove profile user menu code * fix: remove user menu profle code * fix: remove route paths for profiles --- .../src/components/UserMenu/UserMenu.tsx | 26 +------------------ .../components/UserMenuNav/UserMenuNav.tsx | 15 +---------- .../src/containers/AppRoutes/AppRoutes.tsx | 20 +------------- .../RoutesContainer/RoutesContainer.tsx | 19 -------------- 4 files changed, 3 insertions(+), 77 deletions(-) diff --git a/packages/ui-react/src/components/UserMenu/UserMenu.tsx b/packages/ui-react/src/components/UserMenu/UserMenu.tsx index 4c94dbfcb..23f047d90 100644 --- a/packages/ui-react/src/components/UserMenu/UserMenu.tsx +++ b/packages/ui-react/src/components/UserMenu/UserMenu.tsx @@ -7,7 +7,6 @@ import Icon from '../Icon/Icon'; import ProfileCircle from '../ProfileCircle/ProfileCircle'; import Popover from '../Popover/Popover'; import Panel from '../Panel/Panel'; -import ProfilesMenu from '../ProfilesMenu/ProfilesMenu'; import Button from '../Button/Button'; import UserMenuNav from '../UserMenuNav/UserMenuNav'; import HeaderActionButton from '../Header/HeaderActionButton'; @@ -29,20 +28,7 @@ type Props = { onSelectProfile: (params: { id: string; avatarUrl: string }) => void; }; -const UserMenu = ({ - isLoggedIn, - favoritesEnabled, - open, - onClose, - onOpen, - onLoginButtonClick, - onSignUpButtonClick, - profilesEnabled, - profile, - profiles, - profileLoading, - onSelectProfile, -}: Props) => { +const UserMenu = ({ isLoggedIn, favoritesEnabled, open, onClose, onOpen, onLoginButtonClick, onSignUpButtonClick, profilesEnabled, profile }: Props) => { const { t } = useTranslation('menu'); if (!isLoggedIn) { @@ -69,16 +55,6 @@ const UserMenu = ({
- {profilesEnabled && ( - - )}
diff --git a/packages/ui-react/src/components/UserMenuNav/UserMenuNav.tsx b/packages/ui-react/src/components/UserMenuNav/UserMenuNav.tsx index 962ff8298..d1717585e 100644 --- a/packages/ui-react/src/components/UserMenuNav/UserMenuNav.tsx +++ b/packages/ui-react/src/components/UserMenuNav/UserMenuNav.tsx @@ -8,14 +8,12 @@ import AccountCircle from '@jwp/ott-theme/assets/icons/account_circle.svg?react' import Favorite from '@jwp/ott-theme/assets/icons/favorite.svg?react'; import BalanceWallet from '@jwp/ott-theme/assets/icons/balance_wallet.svg?react'; import Exit from '@jwp/ott-theme/assets/icons/exit.svg?react'; -import { userProfileURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { PATH_USER_ACCOUNT, PATH_USER_FAVORITES, PATH_USER_PAYMENTS } from '@jwp/ott-common/src/paths'; import type { Profile } from '@jwp/ott-common/types/profiles'; import styles from '../UserMenu/UserMenu.module.scss'; // TODO inherit styling import MenuButton from '../MenuButton/MenuButton'; import Icon from '../Icon/Icon'; -import ProfileCircle from '../ProfileCircle/ProfileCircle'; type Props = { small?: boolean; @@ -27,7 +25,7 @@ type Props = { favoritesEnabled?: boolean; }; -const UserMenuNav = ({ showPaymentItems, small = false, onButtonClick, currentProfile, favoritesEnabled, focusable, titleId }: Props) => { +const UserMenuNav = ({ showPaymentItems, small = false, onButtonClick, favoritesEnabled, focusable, titleId }: Props) => { const { t } = useTranslation('user'); const navigate = useNavigate(); const accountController = getModule(AccountController); @@ -48,17 +46,6 @@ const UserMenuNav = ({ showPaymentItems, small = false, onButtonClick, currentPr {t('nav.settings')}
    - {currentProfile && ( -
  • - } - /> -
  • - )}
  • }> - } /> - } /> - } /> - } /> } errorElement={}> } /> } /> diff --git a/platforms/web/src/containers/RoutesContainer/RoutesContainer.tsx b/platforms/web/src/containers/RoutesContainer/RoutesContainer.tsx index e9e67ac00..5a78f7cbe 100644 --- a/platforms/web/src/containers/RoutesContainer/RoutesContainer.tsx +++ b/platforms/web/src/containers/RoutesContainer/RoutesContainer.tsx @@ -2,23 +2,14 @@ import React, { useEffect } from 'react'; import { Navigate, useLocation } from 'react-router-dom'; import { Outlet } from 'react-router'; import { useTranslation } from 'react-i18next'; -import { useProfileStore } from '@jwp/ott-common/src/stores/ProfileStore'; -import { useProfiles } from '@jwp/ott-hooks-react/src/useProfiles'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; -import { PATH_USER_PROFILES } from '@jwp/ott-common/src/paths'; import { shallow } from '@jwp/ott-common/src/utils/compare'; -import LoadingOverlay from '@jwp/ott-ui-react/src/components/LoadingOverlay/LoadingOverlay'; -import useQueryParam from '@jwp/ott-ui-react/src/hooks/useQueryParam'; import useNotifications from '#src/hooks/useNotifications'; const RoutesContainer = () => { const { i18n } = useTranslation(); const location = useLocation(); - const userModal = useQueryParam('u'); - - const { profile, selectingProfileAvatar } = useProfileStore(); - const { shouldManageProfiles } = useProfiles(); const userData = useAccountStore((s) => ({ loading: s.loading, user: s.user }), shallow); @@ -36,16 +27,6 @@ const RoutesContainer = () => { return ; // component instead of hook to prevent extra re-renders } - // show a loading overlay when a profile is loading - if (userData.user && selectingProfileAvatar !== null) { - return ; - } - - // the user must choose a profile first - if (shouldManageProfiles && !location.pathname.includes(PATH_USER_PROFILES) && !userModal) { - return ; - } - return ; }; From 6065e0534636fc615666900b93e1a217be47bf88 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Wed, 12 Jun 2024 10:42:31 +0200 Subject: [PATCH 07/44] fix: remove duplicate code --- .../integrations/jwp/JWPCheckoutService.ts | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 3fac780c9..6289f297b 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -81,27 +81,9 @@ export default class JWPCheckoutService extends CheckoutService { return offerId; } - /** - * Parse the given offer id and extract the asset id. - * The offer id might be the Cleeng format (`S_`) or the asset id as string. - */ - private parseOfferId(offerId: string | number) { - if (typeof offerId === 'string') { - // offer id format `S_` - if (offerId.startsWith('C') || offerId.startsWith('S')) { - return parseInt(offerId.slice(1).split('_')[0]); - } - - // offer id format `` - return parseInt(offerId); - } - - return offerId; - } - private formatOffer = (offer: PlanPrice): Offer => { const offerId = offer.access.type === 'subscription' ? `S${offer.id}` : `C${offer.id}`; - + return { id: offer.id, offerId, From 6b0bfe45da3b43d00f555c84bfe31e9f88d544a8 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Wed, 12 Jun 2024 12:46:22 +0200 Subject: [PATCH 08/44] fix: restore formatOfferId method --- .../services/integrations/jwp/JWPCheckoutService.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 6289f297b..c47bb8610 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -63,6 +63,14 @@ export default class JWPCheckoutService extends CheckoutService { }; }; + /** + * Format a (Cleeng like) offer id for the given access fee (pricing option). For JWP, we need the asset id and + * access fee id in some cases. + */ + private formatOfferId(offer: PlanPrice) { + return `${offer.access.type === 'subscription' ? 'S' : 'C'}${offer.id}`; //`${offer.access.type === 'subscription' ? 'S' : 'C'}S${offer.item_id}_${offer.id}`; + } + /** * Parse the given offer id and extract the asset id. * The offer id might be the Cleeng format (`S_`) or the asset id as string. @@ -82,11 +90,9 @@ export default class JWPCheckoutService extends CheckoutService { } private formatOffer = (offer: PlanPrice): Offer => { - const offerId = offer.access.type === 'subscription' ? `S${offer.id}` : `C${offer.id}`; - return { id: offer.id, - offerId, + offerId: this.formatOfferId(offer), offerCurrency: offer.metadata.currency, customerPriceInclTax: offer.metadata.amount, customerCurrency: offer.metadata.currency, From 5379492d9dc0d76b0703f927356296db797b47b3 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Wed, 12 Jun 2024 20:33:30 +0200 Subject: [PATCH 09/44] fix: fix failing check access --- .../integrations/jwp/JWPCheckoutService.ts | 81 ++++++++++--------- packages/common/types/checkout.ts | 2 + packages/common/types/jw.ts | 28 ++++++- .../CheckoutForm/CheckoutForm.test.tsx | 2 +- .../ChooseOfferForm/ChooseOfferForm.test.tsx | 4 +- .../ChooseOfferForm/ChooseOfferForm.tsx | 7 +- .../AccountModal/forms/Checkout.tsx | 5 +- 7 files changed, 83 insertions(+), 46 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index c47bb8610..bf713a026 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -1,7 +1,7 @@ import InPlayer, { type MerchantPaymentMethod } from '@inplayer-org/inplayer.js'; import { inject, injectable } from 'inversify'; -import { type JwPlanPricesResponse, type PlanPrice } from '../../../../../../packages/common/types/jw'; +import type { JwPlanPricesResponse, PlanPrice, PlansListResponse } from '../../../../../../packages/common/types/jw'; import { isSVODOffer } from '../../../utils/offers'; import type { CardPaymentData, @@ -71,37 +71,21 @@ export default class JWPCheckoutService extends CheckoutService { return `${offer.access.type === 'subscription' ? 'S' : 'C'}${offer.id}`; //`${offer.access.type === 'subscription' ? 'S' : 'C'}S${offer.item_id}_${offer.id}`; } - /** - * Parse the given offer id and extract the asset id. - * The offer id might be the Cleeng format (`S_`) or the asset id as string. - */ - private parseOfferId(offerId: string | number) { - if (typeof offerId === 'string') { - // offer id format `S_` - if (offerId.startsWith('C') || offerId.startsWith('S')) { - return parseInt(offerId.slice(1).split('_')[0]); - } - - // offer id format `` - return parseInt(offerId); - } - - return offerId; - } - - private formatOffer = (offer: PlanPrice): Offer => { + private formatOffer = ({ title, planId, planOriginalId, ...offer }: PlanPrice & { title: string; planId: string; planOriginalId: number }): Offer => { return { - id: offer.id, + id: offer.original_id, offerId: this.formatOfferId(offer), + planId, + planOriginalId, offerCurrency: offer.metadata.currency, customerPriceInclTax: offer.metadata.amount, customerCurrency: offer.metadata.currency, - offerTitle: offer.metadata.name, + offerTitle: title, active: true, period: offer.access.period, freePeriods: offer.metadata.trial?.quantity ?? 0, planSwitchEnabled: false, - } as unknown as Offer; + } as Offer; }; private formatOrder = (payload: CreateOrderArgs): Order => { @@ -135,24 +119,43 @@ export default class JWPCheckoutService extends CheckoutService { }; }; + getAppPlans = async (plansIds: (string | number)[]) => { + try { + const response = await this.jwpService.get(`/plans?q=id:(${plansIds.map((planId) => `"${planId}"`).join(' OR ')})`); + return response.plans; + } catch { + throw new Error('Failed to get plans'); + } + }; + getOffers: GetOffers = async (payload) => { - const offers = await Promise.all( - payload.offerIds.map(async (planId) => { - try { - const data = await this.jwpService.get(`/plans/${planId}/prices`); + if (!payload.offerIds.length) { + return []; + } - if (data.prices) { - return data.prices.map((offer) => this.formatOffer(offer)); - } + try { + const plans = await this.getAppPlans(payload.offerIds); - return []; - } catch { - throw new Error('Failed to get offers'); - } - }), - ); + const offers = await Promise.all( + plans.map(async (plan) => { + try { + const data = await this.jwpService.get(`/plans/${plan.id}/prices`); - return offers.flat(); + if (data.prices) { + return data.prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name })); + } + + return []; + } catch { + throw new Error(); + } + }), + ); + + return offers.flat(); + } catch { + throw new Error('Failed to get offers'); + } }; getPaymentMethods: GetPaymentMethods = async () => { @@ -259,9 +262,9 @@ export default class JWPCheckoutService extends CheckoutService { } }; - getEntitlements: GetEntitlements = async ({ offerId }) => { + getEntitlements: GetEntitlements = async ({ offerId: planId }) => { try { - const response = await InPlayer.Asset.checkAccessForAsset(this.parseOfferId(offerId)); + const response = await InPlayer.Asset.checkAccessForAsset(parseInt(planId)); return this.formatEntitlements(response.data.expires_at, true); } catch { return this.formatEntitlements(); diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index 89210f117..110c278dc 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -5,6 +5,8 @@ import type { EmptyEnvironmentServiceRequest, EnvironmentServiceRequest, Promise export type Offer = { id: number | null; offerId: string; + planId: string; + planOriginalId: number; offerPrice: number; offerCurrency: string; offerCurrencySymbol: string; diff --git a/packages/common/types/jw.ts b/packages/common/types/jw.ts index e642912ca..9a85bd4a9 100644 --- a/packages/common/types/jw.ts +++ b/packages/common/types/jw.ts @@ -6,6 +6,32 @@ export type JwListResponse; + exclude: Record; + }; + }; + relationships: { + prices?: { id: string; type: 'price' }[]; + }; + created: string; + last_modified: string; + type: 'plan'; + schema: string; +}; + +export type PlansListResponse = JwListResponse<'plans', PlanDetailsResponse>; + export type PlanPrice = { id: string; access: { @@ -21,7 +47,7 @@ export type PlanPrice = { }; original_id: number; relationships: { - plans: { id: string; type: 'plan' }[]; + plans?: { id: string; type: 'plan' }[]; }; schema: string; type: 'price'; diff --git a/packages/ui-react/src/components/CheckoutForm/CheckoutForm.test.tsx b/packages/ui-react/src/components/CheckoutForm/CheckoutForm.test.tsx index 2f6c1d366..3fe33fae9 100644 --- a/packages/ui-react/src/components/CheckoutForm/CheckoutForm.test.tsx +++ b/packages/ui-react/src/components/CheckoutForm/CheckoutForm.test.tsx @@ -24,7 +24,7 @@ describe('', () => { couponFormError={undefined} couponFormSubmitting={false} order={order as Order} - offer={offer as Offer} + offer={offer as unknown as Offer} offerType={'svod'} submitting={false} > diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.test.tsx b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.test.tsx index 61a00c9a1..2201edbea 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.test.tsx +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.test.tsx @@ -8,8 +8,8 @@ import tvodOffer from '@jwp/ott-testing/fixtures/tvodOffer.json'; import ChooseOfferForm from './ChooseOfferForm'; -const svodOffers = [monthlyOffer, yearlyOffer] as Offer[]; -const tvodOffers = [tvodOffer] as Offer[]; +const svodOffers = [monthlyOffer, yearlyOffer] as unknown as Offer[]; +const tvodOffers = [tvodOffer] as unknown as Offer[]; describe('', () => { test('renders and matches snapshot', () => { diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx index 1cb06148e..293163b53 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx @@ -132,7 +132,12 @@ const ChooseOfferForm: React.FC = ({ values, errors, submitting, offers,
    {Object.keys(groupedOffers).map((period) => ( -
    diff --git a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx index b65abac5f..8b57fad98 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx @@ -42,7 +42,8 @@ const Checkout = () => { onSubmitPaypalPaymentSuccess: ({ redirectUrl }) => { window.location.href = redirectUrl; }, - onSubmitStripePaymentSuccess: () => navigate(modalURLFromLocation(location, 'waiting-for-payment'), { replace: true }), + onSubmitStripePaymentSuccess: () => + navigate(modalURLFromLocation(location, 'waiting-for-payment', { offerId: selectedOffer?.planOriginalId }), { replace: true }), }); const { @@ -102,7 +103,7 @@ const Checkout = () => { } const cancelUrl = modalURLFromWindowLocation('payment-cancelled'); - const waitingUrl = modalURLFromWindowLocation('waiting-for-payment', { offerId: selectedOffer?.offerId }); + const waitingUrl = modalURLFromWindowLocation('waiting-for-payment', { offerId: selectedOffer?.planOriginalId }); const errorUrl = modalURLFromWindowLocation('payment-error'); const successUrlPaypal = offerType === 'svod' ? waitingUrl : createURL(window.location.href, { u: '' }); const referrer = window.location.href; From abfa6ac1ced51bc259c340bca4220e6da5d88344 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Thu, 13 Jun 2024 01:55:34 +0200 Subject: [PATCH 10/44] fix: update access checking with getJWPMediaToken --- packages/hooks-react/src/useEntitlement.ts | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/hooks-react/src/useEntitlement.ts b/packages/hooks-react/src/useEntitlement.ts index caef0a950..11348d274 100644 --- a/packages/hooks-react/src/useEntitlement.ts +++ b/packages/hooks-react/src/useEntitlement.ts @@ -1,4 +1,4 @@ -import { useQueries } from 'react-query'; +import { useQueries, useQuery } from 'react-query'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import type { GetEntitlementsResponse } from '@jwp/ott-common/types/checkout'; import type { MediaOffer } from '@jwp/ott-common/types/media'; @@ -6,6 +6,7 @@ import { getModule } from '@jwp/ott-common/src/modules/container'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; +import JWPEntitlementService from '@jwp/ott-common/src/services/JWPEntitlementService'; import { isLocked } from '@jwp/ott-common/src/utils/entitlements'; import { shallow } from '@jwp/ott-common/src/utils/compare'; @@ -33,7 +34,7 @@ const notifyOnChangeProps = ['data' as const, 'isLoading' as const]; * * */ const useEntitlement: UseEntitlement = (playlistItem) => { - const { accessModel } = useConfigStore(); + const { accessModel, config } = useConfigStore(); const { user, subscription } = useAccountStore( ({ user, subscription }) => ({ user, @@ -43,6 +44,7 @@ const useEntitlement: UseEntitlement = (playlistItem) => { ); const checkoutController = getModule(CheckoutController, false); + const jwpEntitlementService = getModule(JWPEntitlementService); const isPreEntitled = playlistItem && !isLocked(accessModel, !!user, !!subscription, playlistItem); const mediaOffers = playlistItem?.mediaOffers || []; @@ -58,9 +60,26 @@ const useEntitlement: UseEntitlement = (playlistItem) => { })), ); + const { isLoading: isTokenLoading, data: hasAccessToken = false } = useQuery( + ['access', playlistItem?.mediaid], + () => { + if (!playlistItem?.mediaid) { + return; + } + + const token = jwpEntitlementService.getJWPMediaToken(config.id, playlistItem.mediaid); + + return !!token; + }, + { + enabled: !!playlistItem?.mediaid, + }, + ); + // when the user is logged out the useQueries will be disabled but could potentially return its cached data - const isMediaEntitled = !!user && mediaEntitlementQueries.some((item) => item.isSuccess && (item.data as QueryResult)?.responseData?.accessGranted); - const isMediaEntitlementLoading = !isMediaEntitled && mediaEntitlementQueries.some((item) => item.isLoading); + const isMediaEntitled = + hasAccessToken || (!!user && mediaEntitlementQueries.some((item) => item.isSuccess && (item.data as QueryResult)?.responseData?.accessGranted)); + const isMediaEntitlementLoading = !isMediaEntitled && (isTokenLoading || mediaEntitlementQueries.some((item) => item.isLoading)); const isEntitled = !!playlistItem && (isPreEntitled || isMediaEntitled); From e7ff32d006a1c8f2ce808ad0dff77b9144db3e00 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 17 Jun 2024 15:13:23 +0200 Subject: [PATCH 11/44] fix: change base path in jwpbaseservice --- .../src/services/integrations/jwp/JWPCheckoutService.ts | 6 ++++-- .../src/services/integrations/jwp/base/JWPBaseService.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index bf713a026..5e8d6d669 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -121,7 +121,9 @@ export default class JWPCheckoutService extends CheckoutService { getAppPlans = async (plansIds: (string | number)[]) => { try { - const response = await this.jwpService.get(`/plans?q=id:(${plansIds.map((planId) => `"${planId}"`).join(' OR ')})`); + const response = await this.jwpService.get( + `/v3/sites/${this.jwpService.siteId}/plans?q=id:(${plansIds.map((planId) => `"${planId}"`).join(' OR ')})`, + ); return response.plans; } catch { throw new Error('Failed to get plans'); @@ -139,7 +141,7 @@ export default class JWPCheckoutService extends CheckoutService { const offers = await Promise.all( plans.map(async (plan) => { try { - const data = await this.jwpService.get(`/plans/${plan.id}/prices`); + const data = await this.jwpService.get(`/v3/sites/${this.jwpService.siteId}/plans/${plan.id}/prices`); if (data.prices) { return data.prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name })); diff --git a/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts b/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts index 3e5624d0e..e1536675f 100644 --- a/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts +++ b/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts @@ -9,10 +9,10 @@ type RequestOptions = { export default class JWPBaseService { private sandbox = true; - private siteId = ''; + siteId = ''; // private getBaseUrl = () => (this.sandbox ? 'https://staging-v2.inplayer.com' : 'https://services.inplayer.com'); - private getBaseUrl = () => `${this.sandbox ? 'https://services-daily.inplayer.com' : 'https://services.inplayer.com'}/v3/sites/${this.siteId}`; + private getBaseUrl = () => (this.sandbox ? 'https://services-daily.inplayer.com' : 'https://services.inplayer.com'); private performRequest = async (path: string = '/', method = 'GET', body?: string, options: RequestOptions = {}) => { try { From f76c4fe8d68ff1bde7227c30b5af1765f488d143 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Wed, 19 Jun 2024 01:06:43 +0200 Subject: [PATCH 12/44] feat(project): multiple plans --- packages/common/package.json | 2 +- .../src/controllers/CheckoutController.ts | 5 + packages/common/src/modules/register.ts | 2 - .../services/integrations/CheckoutService.ts | 3 + .../cleeng/CleengCheckoutService.ts | 3 + .../integrations/jwp/JWPCheckoutService.ts | 42 ++-- .../integrations/jwp/base/JWPBaseService.ts | 51 ----- packages/common/types/checkout.ts | 2 + packages/hooks-react/package.json | 2 +- .../hooks-react/src/useContentProtection.ts | 23 ++- packages/hooks-react/src/useEntitlement.ts | 22 +-- packages/ui-react/package.json | 2 +- .../ChooseOfferForm.module.scss | 75 +------ .../ChooseOfferForm/ChooseOfferForm.tsx | 119 +---------- .../ChooseOfferForm.test.tsx.snap | 185 +++++++++++++----- .../ChoosePlanForm/ChoosePlanForm.module.scss | 21 ++ .../ChoosePlanForm/ChoosePlanForm.test.tsx | 108 ++++++++++ .../ChoosePlanForm/ChoosePlanForm.tsx | 70 +++++++ .../ChoosePlanForm.test.tsx.snap | 161 +++++++++++++++ .../ListPlans/ListPlans.module.scss | 9 + .../components/ListPlans/ListPlans.test.tsx | 12 ++ .../src/components/ListPlans/ListPlans.tsx | 17 ++ .../__snapshots__/ListPlans.test.tsx.snap | 24 +++ .../components/PriceBox/PriceBox.module.scss | 96 +++++++++ .../src/components/PriceBox/PriceBox.tsx | 126 ++++++++++++ .../containers/AccountModal/AccountModal.tsx | 4 + .../AccountModal/forms/ChooseOffer.tsx | 8 + .../AccountModal/forms/PersonalDetails.tsx | 4 +- .../StartWatchingButton.tsx | 10 +- platforms/web/public/locales/en/video.json | 3 +- platforms/web/public/locales/es/video.json | 3 +- yarn.lock | 8 +- 32 files changed, 872 insertions(+), 350 deletions(-) delete mode 100644 packages/common/src/services/integrations/jwp/base/JWPBaseService.ts create mode 100644 packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.module.scss create mode 100644 packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.test.tsx create mode 100644 packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.tsx create mode 100644 packages/ui-react/src/components/ChoosePlanForm/__snapshots__/ChoosePlanForm.test.tsx.snap create mode 100644 packages/ui-react/src/components/ListPlans/ListPlans.module.scss create mode 100644 packages/ui-react/src/components/ListPlans/ListPlans.test.tsx create mode 100644 packages/ui-react/src/components/ListPlans/ListPlans.tsx create mode 100644 packages/ui-react/src/components/ListPlans/__snapshots__/ListPlans.test.tsx.snap create mode 100644 packages/ui-react/src/components/PriceBox/PriceBox.module.scss create mode 100644 packages/ui-react/src/components/PriceBox/PriceBox.tsx diff --git a/packages/common/package.json b/packages/common/package.json index bda2d81f3..0b1883ecf 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -9,7 +9,7 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@inplayer-org/inplayer.js": "^3.13.24", + "@inplayer-org/inplayer.js": "^3.13.25", "broadcast-channel": "^7.0.0", "date-fns": "^2.28.0", "fast-xml-parser": "^4.3.2", diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 3695e4af9..07f16f729 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import i18next from 'i18next'; import type { + AccessMethod, AddAdyenPaymentDetailsResponse, AdyenPaymentSession, CardPaymentData, @@ -355,4 +356,8 @@ export default class CheckoutController { getEntitlements: GetEntitlements = (payload) => { return this.checkoutService.getEntitlements(payload); }; + + getAccessMethod = (): AccessMethod => { + return this.checkoutService.accessMethod; + }; } diff --git a/packages/common/src/modules/register.ts b/packages/common/src/modules/register.ts index ed61861f2..e26533416 100644 --- a/packages/common/src/modules/register.ts +++ b/packages/common/src/modules/register.ts @@ -45,7 +45,6 @@ import JWPSubscriptionService from '../services/integrations/jwp/JWPSubscription import JWPProfileService from '../services/integrations/jwp/JWPProfileService'; import { getIntegrationType } from './functions/getIntegrationType'; import { isCleengIntegrationType, isJwpIntegrationType } from './functions/calculateIntegrationType'; -import JWPBaseService from '../services/integrations/jwp/base/JWPBaseService'; // Common services container.bind(ConfigService).toSelf(); @@ -84,7 +83,6 @@ container.bind(SubscriptionService).to(CleengSubscriptionService).whenTargetName container.bind(DETERMINE_INTEGRATION_TYPE).toConstantValue(isJwpIntegrationType); container.bind(JWPEntitlementService).toSelf(); container.bind(AccountService).to(JWPAccountService).whenTargetNamed(INTEGRATION.JWP); -container.bind(JWPBaseService).toSelf(); container.bind(CheckoutService).to(JWPCheckoutService).whenTargetNamed(INTEGRATION.JWP); container.bind(SubscriptionService).to(JWPSubscriptionService).whenTargetNamed(INTEGRATION.JWP); container.bind(ProfileService).to(JWPProfileService).whenTargetNamed(INTEGRATION.JWP); diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index 8006e55b3..daa555e72 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -1,4 +1,5 @@ import type { + AccessMethod, AddAdyenPaymentDetails, CreateOrder, DeletePaymentMethod, @@ -25,6 +26,8 @@ import type { Config } from '../../../types/config'; export default abstract class CheckoutService { abstract initialize: (config: Config) => Promise; + abstract accessMethod: AccessMethod; + abstract getOffers: GetOffers; abstract createOrder: CreateOrder; diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index 3c81816a7..f2730d753 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -1,6 +1,7 @@ import { inject, injectable } from 'inversify'; import type { + AccessMethod, AddAdyenPaymentDetails, CreateOrder, CreateOrderPayload, @@ -35,6 +36,8 @@ export default class CleengCheckoutService extends CheckoutService { private readonly cleengService: CleengService; private readonly getCustomerIP: GetCustomerIP; + accessMethod: AccessMethod = 'offer'; + constructor(cleengService: CleengService, @inject(GET_CUSTOMER_IP) getCustomerIP: GetCustomerIP) { super(); this.cleengService = cleengService; diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 5e8d6d669..ff3b6cfdb 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -1,9 +1,10 @@ import InPlayer, { type MerchantPaymentMethod } from '@inplayer-org/inplayer.js'; -import { inject, injectable } from 'inversify'; +import { injectable } from 'inversify'; -import type { JwPlanPricesResponse, PlanPrice, PlansListResponse } from '../../../../../../packages/common/types/jw'; +import type { PlanPrice } from '../../../../../../packages/common/types/jw'; import { isSVODOffer } from '../../../utils/offers'; import type { + AccessMethod, CardPaymentData, CreateOrder, CreateOrderArgs, @@ -24,25 +25,17 @@ import type { Config } from '../../../../types/config'; import CheckoutService from '../CheckoutService'; import type { ServiceResponse } from '../../../../types/service'; import { isCommonError } from '../../../utils/api'; -import AccountService from '../AccountService'; -import { INTEGRATION_TYPE } from '../../../modules/types'; -import { getNamedModule } from '../../../modules/container'; - -import JWPBaseService from './base/JWPBaseService'; @injectable() export default class JWPCheckoutService extends CheckoutService { - private readonly jwpService: JWPBaseService; - private readonly accountService: AccountService; private readonly cardPaymentProvider = 'stripe'; + siteId = ''; - constructor(jwpService: JWPBaseService, @inject(INTEGRATION_TYPE) integrationType: string) { - super(); - this.jwpService = jwpService; - this.accountService = getNamedModule(AccountService, integrationType); - } + accessMethod: AccessMethod = 'plan'; - initialize = async (config: Config) => this.jwpService.setup(this.accountService.sandbox, config.siteId); + initialize = async (config: Config) => { + this.siteId = config.siteId; + }; private formatPaymentMethod = (method: MerchantPaymentMethod, cardPaymentProvider: string): PaymentMethod => { return { @@ -119,12 +112,11 @@ export default class JWPCheckoutService extends CheckoutService { }; }; - getAppPlans = async (plansIds: (string | number)[]) => { + getAppPlans = async (plansIds: string[]) => { try { - const response = await this.jwpService.get( - `/v3/sites/${this.jwpService.siteId}/plans?q=id:(${plansIds.map((planId) => `"${planId}"`).join(' OR ')})`, - ); - return response.plans; + const response = await InPlayer.Payment.getSitePlans(this.siteId, plansIds); + + return response.data.plans; } catch { throw new Error('Failed to get plans'); } @@ -136,15 +128,17 @@ export default class JWPCheckoutService extends CheckoutService { } try { - const plans = await this.getAppPlans(payload.offerIds); + const plans = await this.getAppPlans(payload.offerIds as string[]); const offers = await Promise.all( plans.map(async (plan) => { try { - const data = await this.jwpService.get(`/v3/sites/${this.jwpService.siteId}/plans/${plan.id}/prices`); + const response = await InPlayer.Payment.getSitePlanPrices(this.siteId, plan.id); - if (data.prices) { - return data.prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name })); + if (response.data.prices) { + return response.data.prices.map((offer) => + this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name }), + ); } return []; diff --git a/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts b/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts deleted file mode 100644 index e1536675f..000000000 --- a/packages/common/src/services/integrations/jwp/base/JWPBaseService.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { injectable } from 'inversify'; - -type RequestOptions = { - authenticate?: boolean; - keepalive?: boolean; -}; - -@injectable() -export default class JWPBaseService { - private sandbox = true; - - siteId = ''; - - // private getBaseUrl = () => (this.sandbox ? 'https://staging-v2.inplayer.com' : 'https://services.inplayer.com'); - private getBaseUrl = () => (this.sandbox ? 'https://services-daily.inplayer.com' : 'https://services.inplayer.com'); - - private performRequest = async (path: string = '/', method = 'GET', body?: string, options: RequestOptions = {}) => { - try { - const resp = await fetch(`${this.getBaseUrl()}${path}`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - keepalive: options.keepalive, - method, - body, - }); - - return await resp.json(); - } catch (error: unknown) { - return { - errors: Array.of(error instanceof Error ? error.message : String(error)), - }; - } - }; - - setup = (sandbox: boolean, siteId: string) => { - this.sandbox = sandbox; - this.siteId = siteId; - }; - - get = (path: string, options?: RequestOptions) => this.performRequest(path, 'GET', undefined, options) as Promise; - - patch = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'PATCH', body, options) as Promise; - - put = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'PUT', body, options) as Promise; - - post = (path: string, body?: string, options?: RequestOptions) => this.performRequest(path, 'POST', body, options) as Promise; - - remove = (path: string, options?: RequestOptions) => this.performRequest(path, 'DELETE', undefined, options) as Promise; -} diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index 110c278dc..abbf887ff 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -48,6 +48,8 @@ export type Offer = { export type OfferType = 'svod' | 'tvod'; +export type AccessMethod = 'offer' | 'plan'; + export type ChooseOfferFormData = { selectedOfferType?: OfferType; selectedOfferId?: string; diff --git a/packages/hooks-react/package.json b/packages/hooks-react/package.json index f0573aeda..37efac67d 100644 --- a/packages/hooks-react/package.json +++ b/packages/hooks-react/package.json @@ -9,7 +9,7 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@inplayer-org/inplayer.js": "^3.13.24", + "@inplayer-org/inplayer.js": "^3.13.25", "date-fns": "^2.28.0", "i18next": "^22.4.15", "planby": "^0.3.0", diff --git a/packages/hooks-react/src/useContentProtection.ts b/packages/hooks-react/src/useContentProtection.ts index a3598af93..af5eaf9fc 100644 --- a/packages/hooks-react/src/useContentProtection.ts +++ b/packages/hooks-react/src/useContentProtection.ts @@ -8,6 +8,7 @@ import { getModule } from '@jwp/ott-common/src/modules/container'; import AccountController from '@jwp/ott-common/src/controllers/AccountController'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; +import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; const useContentProtection = ( type: EntitlementType, @@ -19,8 +20,11 @@ const useContentProtection = ( ) => { const genericEntitlementService = getModule(GenericEntitlementService); const jwpEntitlementService = getModule(JWPEntitlementService); + const checkoutController = getModule(CheckoutController); - const { configId, signingConfig, contentProtection, jwp, urlSigning } = useConfigStore(({ config }) => ({ + const accessMethod = checkoutController.getAccessMethod(); + + const { configId, signingConfig, contentProtection, urlSigning } = useConfigStore(({ config }) => ({ configId: config.id, signingConfig: config.contentSigningService, contentProtection: config.contentProtection, @@ -34,20 +38,23 @@ const useContentProtection = ( const { data: token, isLoading } = useQuery( ['token', type, id, params], async () => { - // if provider is not JWP - if (!!id && !!host) { + if (!id) { + return; + } + + if (accessMethod === 'plan') { + return jwpEntitlementService.getJWPMediaToken(configId, id); + } + + if (host) { const accountController = getModule(AccountController); const authData = await accountController.getAuthData(); const { host, drmPolicyId } = signingConfig; return genericEntitlementService.getMediaToken(host, id, authData?.jwt, params, drmPolicyId); } - // if provider is JWP - if (jwp && configId && !!id && signingEnabled) { - return jwpEntitlementService.getJWPMediaToken(configId, id); - } }, - { enabled: signingEnabled && enabled && !!id, keepPreviousData: false, staleTime: 15 * 60 * 1000 }, + { enabled: !!id && (accessMethod === 'plan' || (signingEnabled && enabled)), keepPreviousData: false, staleTime: 15 * 60 * 1000 }, ); const queryResult = useQuery([type, id, params, token], async () => callback(token, drmPolicyId), { diff --git a/packages/hooks-react/src/useEntitlement.ts b/packages/hooks-react/src/useEntitlement.ts index 11348d274..ddf2aa233 100644 --- a/packages/hooks-react/src/useEntitlement.ts +++ b/packages/hooks-react/src/useEntitlement.ts @@ -46,7 +46,9 @@ const useEntitlement: UseEntitlement = (playlistItem) => { const checkoutController = getModule(CheckoutController, false); const jwpEntitlementService = getModule(JWPEntitlementService); - const isPreEntitled = playlistItem && !isLocked(accessModel, !!user, !!subscription, playlistItem); + const accessMethod = checkoutController?.getAccessMethod(); + + const isPreEntitled = accessMethod === 'plan' && playlistItem && !isLocked(accessModel, !!user, !!subscription, playlistItem); const mediaOffers = playlistItem?.mediaOffers || []; // this query is invalidated when the subscription gets reloaded @@ -60,25 +62,21 @@ const useEntitlement: UseEntitlement = (playlistItem) => { })), ); - const { isLoading: isTokenLoading, data: hasAccessToken = false } = useQuery( - ['access', playlistItem?.mediaid], - () => { + const { isLoading: isTokenLoading, data: token } = useQuery( + ['token', 'media', playlistItem?.mediaid, {}], + async () => { if (!playlistItem?.mediaid) { - return; + return ''; } - const token = jwpEntitlementService.getJWPMediaToken(config.id, playlistItem.mediaid); - - return !!token; - }, - { - enabled: !!playlistItem?.mediaid, + return await jwpEntitlementService.getJWPMediaToken(config.id, playlistItem.mediaid); }, + { enabled: accessMethod === 'plan', keepPreviousData: false, staleTime: 15 * 60 * 1000, retry: 2 }, ); // when the user is logged out the useQueries will be disabled but could potentially return its cached data const isMediaEntitled = - hasAccessToken || (!!user && mediaEntitlementQueries.some((item) => item.isSuccess && (item.data as QueryResult)?.responseData?.accessGranted)); + !!token || (!!user && mediaEntitlementQueries.some((item) => item.isSuccess && (item.data as QueryResult)?.responseData?.accessGranted)); const isMediaEntitlementLoading = !isMediaEntitled && (isTokenLoading || mediaEntitlementQueries.some((item) => item.isLoading)); const isEntitled = !!playlistItem && (isPreEntitled || isMediaEntitled); diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 30b484b33..7f96a84ae 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@adyen/adyen-web": "^5.42.1", - "@inplayer-org/inplayer.js": "^3.13.24", + "@inplayer-org/inplayer.js": "^3.13.25", "classnames": "^2.3.1", "date-fns": "^2.28.0", "dompurify": "^2.3.8", diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss index a7b4288c4..975acda24 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.module.scss @@ -4,7 +4,7 @@ @use '@jwp/ott-ui-react/src/styles/mixins/responsive'; .title { - margin-bottom: 16px; + margin-bottom: 8px; font-weight: var(--body-font-weight-bold); font-size: 24px; } @@ -15,12 +15,6 @@ font-size: 18px; } -.tabs { - display: flex; - justify-content: center; - margin-bottom: 16px; -} - .offerGroupSwitch { display: flex; flex: 1; @@ -37,22 +31,7 @@ .offers { display: flex; - justify-content: space-around; margin: 0 -4px 24px; - padding-bottom: 8px; - overflow-x: auto; -} - -.offer { - flex: 1; - min-width: 200px; - max-width: 250px; - margin: 0 4px; - - &:focus-within .label { - @include accessibility.accessibleOutlineContrast; - transform: scale(1.03); - } } .radio { @@ -96,55 +75,3 @@ } } -.offerTitle { - font-weight: var(--body-font-weight-bold); - font-size: 20px; - text-align: center; -} - -.offerDivider { - width: 100%; - border: none; - border-bottom: 1px solid currentColor; - opacity: 0.54; -} - -.offerBenefits { - margin-bottom: 16px; - padding: 0; - - > li { - display: flex; - align-items: center; - margin-bottom: 4px; - padding: 4px 0; - - > svg { - flex-shrink: 0; - margin-right: 4px; - fill: variables.$green; - } - - @include responsive.mobile-only() { - font-size: 14px; - } - } -} - -.fill { - flex: 1; -} - -.offerPrice { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: baseline; - font-size: 32px; - - > small { - margin-left: 4px; - font-size: 12px; - } -} - diff --git a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx index 293163b53..e15ad3b3f 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx +++ b/packages/ui-react/src/components/ChooseOfferForm/ChooseOfferForm.tsx @@ -1,107 +1,19 @@ -import React, { useState, type FC, type SVGProps, useMemo } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import type { FormErrors } from '@jwp/ott-common/types/form'; import type { Offer, ChooseOfferFormData } from '@jwp/ott-common/types/checkout'; -import { getOfferPrice, isSVODOffer } from '@jwp/ott-common/src/utils/offers'; +import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { testId } from '@jwp/ott-common/src/utils/common'; -import CheckCircle from '@jwp/ott-theme/assets/icons/check_circle.svg?react'; import Button from '../Button/Button'; -import ButtonGroup from '../Button/ButtonGroup'; import FormFeedback from '../FormFeedback/FormFeedback'; import DialogBackButton from '../DialogBackButton/DialogBackButton'; import LoadingOverlay from '../LoadingOverlay/LoadingOverlay'; -import Icon from '../Icon/Icon'; +import PriceBox from '../PriceBox/PriceBox'; import styles from './ChooseOfferForm.module.scss'; -type OfferBoxProps = { - offer: Offer; - selected: boolean; -} & Pick; - -const OfferBox: React.FC = ({ offer, selected, onChange }: OfferBoxProps) => { - const { t } = useTranslation('account'); - - const getFreeTrialText = (offer: Offer) => { - if (offer.freeDays) { - return t('choose_offer.benefits.first_days_free', { count: offer.freeDays }); - } else if (offer.freePeriods) { - // t('periods.day', { count }) - // t('periods.week', { count }) - // t('periods.month', { count }) - // t('periods.year', { count }) - const period = t(`periods.${offer.period}`, { count: offer.freePeriods }); - - return t('choose_offer.benefits.first_periods_free', { count: offer.freePeriods, period }); - } - - return null; - }; - - const renderListItem = (text: string, icon: FC>) => ( -
  • - - {text} - . -
  • - ); - - const renderOption = ({ title, periodString, secondBenefit }: { title: string; periodString?: string; secondBenefit?: string }) => ( -
    - -
    -
    - ); - - if (isSVODOffer(offer)) { - const isMonthly = offer.period === 'month'; - - return renderOption({ - title: offer.offerTitle, - secondBenefit: t('choose_offer.benefits.cancel_anytime'), - periodString: isMonthly ? t('periods.month') : t('periods.year'), - }); - } - - return renderOption({ - title: offer.offerTitle, - secondBenefit: - !!offer.durationPeriod && !!offer.durationAmount - ? t('choose_offer.tvod_access', { period: offer.durationPeriod, count: offer.durationAmount }) - : undefined, - }); -}; - -type OfferPeriod = 'month' | 'year'; - type Props = { values: ChooseOfferFormData; errors: FormErrors; @@ -114,33 +26,16 @@ type Props = { }; const ChooseOfferForm: React.FC = ({ values, errors, submitting, offers, showOfferTypeSwitch, onChange, onSubmit, onBackButtonClickHandler }: Props) => { + const siteName = useConfigStore((s) => s.config.siteName); const { t } = useTranslation('account'); const { selectedOfferType, selectedOfferId } = values; - const groupedOffers = useMemo( - () => offers.reduce((acc, offer) => ({ ...acc, [offer.period]: [...(acc[offer.period as OfferPeriod] || []), offer] }), {} as Record), - [offers], - ); - - const [offerFilter, setOfferFilter] = useState(() => Object.keys(groupedOffers)[0] as OfferPeriod); - return ( {onBackButtonClickHandler ? : null}

    {t('choose_offer.title')}

    +

    {t('choose_offer.watch_this_on_platform', { siteName })}

    {errors.form ? {errors.form} : null} -
    - - {Object.keys(groupedOffers).map((period) => ( -
    {showOfferTypeSwitch && (
    = ({ values, errors, submitting, offers, {!offers.length ? (

    {t('choose_offer.no_pricing_available')}

    ) : ( - groupedOffers[offerFilter].map((offer) => ( - - )) + offers.map((offer) => ) )}
    {submitting && } diff --git a/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap b/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap index 310fb7983..ddd8c6888 100644 --- a/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap +++ b/packages/ui-react/src/components/ChooseOfferForm/__snapshots__/ChooseOfferForm.test.tsx.snap @@ -11,30 +11,11 @@ exports[` > renders and matches snapshot 1`] = ` > choose_offer.title -
    -
    - - -
    -
    + choose_offer.watch_this_on_platform +

    @@ -71,11 +52,11 @@ exports[` > renders and matches snapshot 1`] = ` >
    > renders and matches snapshot 1`] = ` value="S916977979_NL" />
    + )} + + )}

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

    From c87a53224b87a2770a0347ee8b06df0b93dd3cd2 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Thu, 20 Jun 2024 16:58:08 +0200 Subject: [PATCH 23/44] fix: update sdk version --- packages/common/package.json | 2 +- packages/common/src/env.ts | 2 +- packages/hooks-react/package.json | 2 +- packages/ui-react/package.json | 2 +- packages/ui-react/src/components/Payment/Payment.tsx | 5 +---- .../ui-react/src/containers/AccountModal/AccountModal.tsx | 2 +- .../containers/AccountModal/forms/CancelSubscription.tsx | 4 ++-- platforms/web/.env | 2 +- platforms/web/public/locales/en/account.json | 8 ++++---- yarn.lock | 8 ++++---- 10 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index ef93522c7..be8cf1aeb 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -9,7 +9,7 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@inplayer-org/inplayer.js": "^3.13.27", + "@inplayer-org/inplayer.js": "^3.13.28", "broadcast-channel": "^7.0.0", "date-fns": "^2.28.0", "fast-xml-parser": "^4.3.2", diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index 7ce25d4c8..684eb0e85 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -15,7 +15,7 @@ export type Env = { const env: Env = { APP_VERSION: '', APP_API_BASE_URL: 'https://content-portal.jwplatform.com', - APP_PLAYER_ID: 'M4qoGvUk', + APP_PLAYER_ID: 'ov7MiL14', APP_FOOTER_TEXT: '', APP_DEFAULT_LANGUAGE: 'en', }; diff --git a/packages/hooks-react/package.json b/packages/hooks-react/package.json index 7980a315a..34d2a9f15 100644 --- a/packages/hooks-react/package.json +++ b/packages/hooks-react/package.json @@ -9,7 +9,7 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@inplayer-org/inplayer.js": "^3.13.27", + "@inplayer-org/inplayer.js": "^3.13.28", "date-fns": "^2.28.0", "i18next": "^22.4.15", "planby": "^0.3.0", diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 33f4bc4dc..17ea3ea6b 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@adyen/adyen-web": "^5.42.1", - "@inplayer-org/inplayer.js": "^3.13.27", + "@inplayer-org/inplayer.js": "^3.13.28", "classnames": "^2.3.1", "date-fns": "^2.28.0", "dompurify": "^2.3.8", diff --git a/packages/ui-react/src/components/Payment/Payment.tsx b/packages/ui-react/src/components/Payment/Payment.tsx index 59f11c818..0faacb697 100644 --- a/packages/ui-react/src/components/Payment/Payment.tsx +++ b/packages/ui-react/src/components/Payment/Payment.tsx @@ -239,10 +239,7 @@ const Payment = ({ /> ) )} - {(activeSubscription.status === 'active' || activeSubscription.status === 'active_trial') && - !isGrantedSubscription && - !isChangingOffer && - canRenewSubscription ? ( + {(activeSubscription.status === 'active' || activeSubscription.status === 'active_trial') && !isGrantedSubscription && !isChangingOffer ? ( +
    Date: Mon, 24 Jun 2024 10:15:29 +0200 Subject: [PATCH 26/44] fix: do not display change subscription button for plans --- .../common/src/controllers/CheckoutController.ts | 2 +- packages/common/src/stores/CheckoutStore.ts | 4 +++- packages/hooks-react/src/useContentProtection.ts | 7 +++---- packages/hooks-react/src/useEntitlement.ts | 3 ++- .../ui-react/src/components/Payment/Payment.tsx | 5 ++++- .../containers/AccountModal/forms/ChooseOffer.tsx | 7 +++---- .../StartWatchingButton/StartWatchingButton.tsx | 14 +++++--------- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 07f16f729..282b5a493 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -46,7 +46,7 @@ export default class CheckoutController { initialiseOffers = async () => { const requestedMediaOffers = useCheckoutStore.getState().requestedMediaOffers; const mediaOffers = requestedMediaOffers ? await this.getOffers({ offerIds: requestedMediaOffers.map(({ offerId }) => offerId) }) : []; - useCheckoutStore.setState({ mediaOffers }); + useCheckoutStore.setState({ mediaOffers, accessMethod: this.getAccessMethod() }); if (!useCheckoutStore.getState().subscriptionOffers.length && this.accountService.svodOfferIds) { const subscriptionOffers = await this.getOffers({ offerIds: this.accountService.svodOfferIds }); diff --git a/packages/common/src/stores/CheckoutStore.ts b/packages/common/src/stores/CheckoutStore.ts index 0fa3fddaf..b1d54149f 100644 --- a/packages/common/src/stores/CheckoutStore.ts +++ b/packages/common/src/stores/CheckoutStore.ts @@ -1,4 +1,4 @@ -import type { Offer, Order, PaymentMethod } from '../../types/checkout'; +import type { AccessMethod, Offer, Order, PaymentMethod } from '../../types/checkout'; import type { MediaOffer } from '../../types/media'; import { createStore } from './utils'; @@ -6,6 +6,7 @@ import { createStore } from './utils'; type CheckoutStore = { requestedMediaOffers: MediaOffer[]; mediaOffers: Offer[]; + accessMethod: AccessMethod; subscriptionOffers: Offer[]; switchSubscriptionOffers: Offer[]; selectedOffer: Offer | null; @@ -20,6 +21,7 @@ type CheckoutStore = { export const useCheckoutStore = createStore('CheckoutStore', (set) => ({ requestedMediaOffers: [], mediaOffers: [], + accessMethod: 'offer', subscriptionOffers: [], switchSubscriptionOffers: [], selectedOffer: null, diff --git a/packages/hooks-react/src/useContentProtection.ts b/packages/hooks-react/src/useContentProtection.ts index af5eaf9fc..34177ec0a 100644 --- a/packages/hooks-react/src/useContentProtection.ts +++ b/packages/hooks-react/src/useContentProtection.ts @@ -7,8 +7,8 @@ import JWPEntitlementService from '@jwp/ott-common/src/services/JWPEntitlementSe import { getModule } from '@jwp/ott-common/src/modules/container'; import AccountController from '@jwp/ott-common/src/controllers/AccountController'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; -import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; const useContentProtection = ( type: EntitlementType, @@ -18,11 +18,10 @@ const useContentProtection = ( enabled: boolean = true, placeholderData?: T, ) => { + const accessMethod = useCheckoutStore((state) => state.accessMethod); + const genericEntitlementService = getModule(GenericEntitlementService); const jwpEntitlementService = getModule(JWPEntitlementService); - const checkoutController = getModule(CheckoutController); - - const accessMethod = checkoutController.getAccessMethod(); const { configId, signingConfig, contentProtection, urlSigning } = useConfigStore(({ config }) => ({ configId: config.id, diff --git a/packages/hooks-react/src/useEntitlement.ts b/packages/hooks-react/src/useEntitlement.ts index c1e52ea0e..8426379f8 100644 --- a/packages/hooks-react/src/useEntitlement.ts +++ b/packages/hooks-react/src/useEntitlement.ts @@ -6,6 +6,7 @@ import type { MediaOffer } from '@jwp/ott-common/types/media'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; +import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; import JWPEntitlementService from '@jwp/ott-common/src/services/JWPEntitlementService'; import { isLocked } from '@jwp/ott-common/src/utils/entitlements'; @@ -47,7 +48,7 @@ const useEntitlement: UseEntitlement = (playlistItem) => { const checkoutController = getModule(CheckoutController, false); const jwpEntitlementService = getModule(JWPEntitlementService); - const accessMethod = checkoutController?.getAccessMethod(); + const accessMethod = useCheckoutStore((state) => state.accessMethod); const isPreEntitled = accessMethod !== 'plan' && playlistItem && !isLocked(accessModel, !!user, !!subscription, playlistItem); const mediaOffers = useMemo(() => playlistItem?.mediaOffers || [], [playlistItem?.mediaOffers]); diff --git a/packages/ui-react/src/components/Payment/Payment.tsx b/packages/ui-react/src/components/Payment/Payment.tsx index 0faacb697..35b37050c 100644 --- a/packages/ui-react/src/components/Payment/Payment.tsx +++ b/packages/ui-react/src/components/Payment/Payment.tsx @@ -6,6 +6,7 @@ import type { Customer } from '@jwp/ott-common/types/account'; import type { Offer } from '@jwp/ott-common/types/checkout'; import type { PaymentDetail, Subscription, Transaction } from '@jwp/ott-common/types/subscription'; import { formatLocalizedDate, formatPrice } from '@jwp/ott-common/src/utils/formatting'; +import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; import ExternalLink from '@jwp/ott-theme/assets/icons/external_link.svg?react'; import PayPal from '@jwp/ott-theme/assets/icons/paypal.svg?react'; @@ -104,6 +105,7 @@ const Payment = ({ const isGrantedSubscription = activeSubscription?.period === 'granted'; const breakpoint = useBreakpoint(); const isMobile = breakpoint === Breakpoint.xs; + const accessMethod = useCheckoutStore((state) => state.accessMethod); const [isChangingOffer, setIsChangingOffer] = useState(false); @@ -159,7 +161,8 @@ const Payment = ({ } } - const showChangeSubscriptionButton = (!isExternalPaymentProvider && offerSwitchesAvailable) || (!isChangingOffer && !canRenewSubscription); + const showChangeSubscriptionButton = + accessMethod !== 'plan' && ((!isExternalPaymentProvider && offerSwitchesAvailable) || (!isChangingOffer && !canRenewSubscription)); return ( <> diff --git a/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx b/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx index f08f4ee6d..5e165f5b7 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/ChooseOffer.tsx @@ -7,8 +7,7 @@ import { modalURLFromLocation } from '@jwp/ott-ui-react/src/utils/location'; import useOffers from '@jwp/ott-hooks-react/src/useOffers'; import useForm from '@jwp/ott-hooks-react/src/useForm'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; -import { getModule } from '@jwp/ott-common/src/modules/container'; -import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; +import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import ChooseOfferForm from '../../../components/ChooseOfferForm/ChooseOfferForm'; import LoadingOverlay from '../../../components/LoadingOverlay/LoadingOverlay'; @@ -21,7 +20,7 @@ const ChooseOffer = () => { const { t } = useTranslation('account'); const isSwitch = useQueryParam('u') === 'upgrade-subscription'; const isPendingOffer = useAccountStore(({ pendingOffer }) => ({ isPendingOffer: !!pendingOffer })); - const checkoutController = getModule(CheckoutController); + const accessMethod = useCheckoutStore((state) => state.accessMethod); const { isLoading, mediaOffers, subscriptionOffers, switchSubscriptionOffers, defaultOfferType, hasMultipleOfferTypes, chooseOffer, switchSubscription } = useOffers(); @@ -91,7 +90,7 @@ const ChooseOffer = () => { ); } - if (checkoutController.getAccessMethod() === 'plan') { + if (accessMethod === 'plan') { return ; } diff --git a/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx b/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx index d0627359b..6dacb0c06 100644 --- a/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx +++ b/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx @@ -11,8 +11,6 @@ import useEntitlement from '@jwp/ott-hooks-react/src/useEntitlement'; import Play from '@jwp/ott-theme/assets/icons/play.svg?react'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; -import { getModule } from '@jwp/ott-common/src/modules/container'; -import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; import Button from '../../components/Button/Button'; import Icon from '../../components/Icon/Icon'; @@ -32,8 +30,6 @@ const StartWatchingButton: React.VFC = ({ item, playUrl, disabled = false const location = useLocation(); const breakpoint = useBreakpoint(); - const checkoutController = getModule(CheckoutController); - // account const accessModel = useConfigStore((state) => state.accessModel); const user = useAccountStore((state) => state.user); @@ -44,7 +40,7 @@ const StartWatchingButton: React.VFC = ({ item, playUrl, disabled = false const videoProgress = watchHistoryItem?.progress; // entitlement - const { setRequestedMediaOffers, subscriptionOffers } = useCheckoutStore(); + const { setRequestedMediaOffers, subscriptionOffers, accessMethod } = useCheckoutStore(); const { isEntitled, mediaOffers } = useEntitlement(item); const hasMediaOffers = !!mediaOffers.length; @@ -52,10 +48,10 @@ const StartWatchingButton: React.VFC = ({ item, playUrl, disabled = false if (isEntitled) return typeof videoProgress === 'number' ? t('continue_watching') : t('start_watching'); if (hasMediaOffers) return t('buy'); if (!isLoggedIn) return t('sign_up_to_start_watching'); - if (checkoutController.getAccessMethod() === 'plan' && subscriptionOffers.length) return t('show_plans'); + if (accessMethod === 'plan' && subscriptionOffers.length) return t('show_plans'); return t('complete_your_subscription'); - }, [checkoutController, isEntitled, isLoggedIn, hasMediaOffers, videoProgress, subscriptionOffers, t]); + }, [accessMethod, isEntitled, isLoggedIn, hasMediaOffers, videoProgress, subscriptionOffers, t]); const handleStartWatchingClick = useCallback(() => { if (isEntitled) { @@ -67,12 +63,12 @@ const StartWatchingButton: React.VFC = ({ item, playUrl, disabled = false } if (!isLoggedIn) return navigate(modalURLFromLocation(location, 'create-account')); if (hasMediaOffers) return navigate(modalURLFromLocation(location, 'choose-offer')); - if (checkoutController.getAccessMethod() === 'plan' && subscriptionOffers.length) { + if (accessMethod === 'plan' && subscriptionOffers.length) { return navigate(modalURLFromLocation(location, 'list-plans')); } return navigate('/u/payments'); - }, [checkoutController, isEntitled, playUrl, navigate, isLoggedIn, location, hasMediaOffers, subscriptionOffers, onClick]); + }, [accessMethod, isEntitled, playUrl, navigate, isLoggedIn, location, hasMediaOffers, subscriptionOffers, onClick]); useEffect(() => { // set the TVOD mediaOffers in the checkout store From 2db2e073f2fc93e9fcf956aaf2993a97c7fddcae Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 11:23:57 +0200 Subject: [PATCH 27/44] fix: implement redirection to media --- .../services/integrations/jwp/JWPCheckoutService.ts | 8 ++++---- packages/ui-react/src/components/Button/Button.tsx | 4 +++- .../ui-react/src/components/ListPlans/ListPlans.tsx | 13 +++++++++++-- .../ui-react/src/components/Payment/Payment.tsx | 6 ++++-- .../ui-react/src/components/PlanBox/PlanBox.tsx | 6 +++--- .../AccountModal/forms/CancelSubscription.tsx | 10 +++++++++- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index fa8e60c7f..454f545fc 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -115,7 +115,7 @@ export default class JWPCheckoutService extends CheckoutService { getPlans = async (searchString: string) => { const response = await InPlayer.Payment.getSitePlans(this.siteId, searchString); - const plans = response.data.plans.filter((plan) => plan.metadata.access_model === 'svod'); + const plans = (response.data.plans || []).filter((plan) => plan.metadata.access_model === 'svod'); return plans; }; @@ -123,7 +123,7 @@ export default class JWPCheckoutService extends CheckoutService { getPlanPrices = async (planId: string) => { const response = await InPlayer.Payment.getSitePlanPrices(this.siteId, planId); - return response.data.prices; + return response.data.prices || []; }; getPlansWithPriceOffers = async (searchString: string) => { @@ -135,7 +135,7 @@ export default class JWPCheckoutService extends CheckoutService { try { const prices = await this.getPlanPrices(plan.id); - if (prices?.length) { + if (prices.length) { const offers = prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name }), ); @@ -165,7 +165,7 @@ export default class JWPCheckoutService extends CheckoutService { try { const prices = await this.getPlanPrices(plan.id); - if (prices) { + if (prices.length) { return prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name })); } diff --git a/packages/ui-react/src/components/Button/Button.tsx b/packages/ui-react/src/components/Button/Button.tsx index aa4844198..5d89f72c8 100644 --- a/packages/ui-react/src/components/Button/Button.tsx +++ b/packages/ui-react/src/components/Button/Button.tsx @@ -29,6 +29,7 @@ type Props = { busy?: boolean; id?: string; activeClassname?: string; + navLinkState?: any; } & React.AriaAttributes; const Button: React.FC = ({ @@ -47,6 +48,7 @@ const Button: React.FC = ({ onClick, className, activeClassname = '', + navLinkState, ...rest }: Props) => { const buttonClassName = (isActive: boolean) => @@ -70,7 +72,7 @@ const Button: React.FC = ({ if (to) { return ( - buttonClassName(isActive)} to={to} {...rest} end> + buttonClassName(isActive)} to={to} state={navLinkState} {...rest} end> {content} ); diff --git a/packages/ui-react/src/components/ListPlans/ListPlans.tsx b/packages/ui-react/src/components/ListPlans/ListPlans.tsx index d65cd874e..539c6f524 100644 --- a/packages/ui-react/src/components/ListPlans/ListPlans.tsx +++ b/packages/ui-react/src/components/ListPlans/ListPlans.tsx @@ -1,6 +1,6 @@ import React, { useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMatch } from 'react-router'; +import { useLocation, useMatch } from 'react-router'; import { useQueryClient } from 'react-query'; import usePlansForMedia from '@jwp/ott-hooks-react/src/usePlansForMedia'; import LoadingOverlay from '@jwp/ott-ui-react/src/components/LoadingOverlay/LoadingOverlay'; @@ -16,6 +16,7 @@ const ListPlans: React.FC = () => { const queryClient = useQueryClient(); const match = useMatch(PATH_MEDIA); + const location = useLocation(); const mediaId = match?.params.id || ''; @@ -44,7 +45,15 @@ const ListPlans: React.FC = () => { ))}
    -
    ); }; diff --git a/packages/ui-react/src/components/Payment/Payment.tsx b/packages/ui-react/src/components/Payment/Payment.tsx index 35b37050c..4e1f951fe 100644 --- a/packages/ui-react/src/components/Payment/Payment.tsx +++ b/packages/ui-react/src/components/Payment/Payment.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import type { AccessModel } from '@jwp/ott-common/types/config'; @@ -128,6 +128,8 @@ const Payment = ({ } }, [selectedOfferId, offers, activeSubscription, setIsUpgradeOffer]); + const locationStateRef = useRef(location.state); + function onCompleteSubscriptionClick() { navigate(modalURLFromLocation(location, 'choose-offer')); } @@ -137,7 +139,7 @@ const Payment = ({ } function onCancelSubscriptionClick() { - navigate(modalURLFromLocation(location, 'unsubscribe')); + navigate(modalURLFromLocation(location, 'unsubscribe'), { state: locationStateRef.current }); } function onRenewSubscriptionClick() { diff --git a/packages/ui-react/src/components/PlanBox/PlanBox.tsx b/packages/ui-react/src/components/PlanBox/PlanBox.tsx index d65a367f5..9b2737050 100644 --- a/packages/ui-react/src/components/PlanBox/PlanBox.tsx +++ b/packages/ui-react/src/components/PlanBox/PlanBox.tsx @@ -1,4 +1,4 @@ -import type { FC, SVGProps } from 'react'; +import { type FC, type SVGProps, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import type { Offer } from '@jwp/ott-common/types/checkout'; import { getOfferPrice } from '@jwp/ott-common/src/utils/offers'; @@ -47,7 +47,7 @@ const PlanBox: FC = ({ plan, prices }) => {
    {prices.map((price, i, array) => ( - <> +
    {getOfferPrice(price)} /{price.period === 'month' ? t('periods.month') : t('periods.year')}
    @@ -56,7 +56,7 @@ const PlanBox: FC = ({ plan, prices }) => { {t('list_plans.or')}
    )} - + ))}
    diff --git a/packages/ui-react/src/containers/AccountModal/forms/CancelSubscription.tsx b/packages/ui-react/src/containers/AccountModal/forms/CancelSubscription.tsx index 47222c38f..742f959fe 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/CancelSubscription.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/CancelSubscription.tsx @@ -48,12 +48,20 @@ const CancelSubscription = () => { navigate(modalURLFromLocation(location, null), { replace: true }); }; + const onFinishUnsubscription = () => { + if (location.state?.returnToPathname) { + navigate(location.state.returnToPathname, { replace: true }); + } else { + closeHandler(); + } + }; + if (!subscription) return null; return (
    {cancelled ? ( - + ) : ( )} From bb64cb89d44f94a32882d61d31d4ff79a29dcd96 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 11:42:17 +0200 Subject: [PATCH 28/44] fix: invoke useoffers in startwatchingbutton --- .../src/containers/StartWatchingButton/StartWatchingButton.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx b/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx index 6dacb0c06..206adc7a7 100644 --- a/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx +++ b/packages/ui-react/src/containers/StartWatchingButton/StartWatchingButton.tsx @@ -8,6 +8,7 @@ import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; import { modalURLFromLocation } from '@jwp/ott-ui-react/src/utils/location'; import useBreakpoint, { Breakpoint } from '@jwp/ott-ui-react/src/hooks/useBreakpoint'; import useEntitlement from '@jwp/ott-hooks-react/src/useEntitlement'; +import useOffers from '@jwp/ott-hooks-react/src/useOffers'; import Play from '@jwp/ott-theme/assets/icons/play.svg?react'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; @@ -39,6 +40,8 @@ const StartWatchingButton: React.VFC = ({ item, playUrl, disabled = false const watchHistoryItem = useWatchHistoryStore((state) => item && state.getItem(item)); const videoProgress = watchHistoryItem?.progress; + useOffers(); + // entitlement const { setRequestedMediaOffers, subscriptionOffers, accessMethod } = useCheckoutStore(); const { isEntitled, mediaOffers } = useEntitlement(item); From 9592547ae2c7353a1ab1b74944bc1862a758946e Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 12:32:45 +0200 Subject: [PATCH 29/44] fix: remove a redundant comment --- .../common/src/services/integrations/jwp/JWPCheckoutService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 454f545fc..8c828e4ad 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -61,7 +61,7 @@ export default class JWPCheckoutService extends CheckoutService { * access fee id in some cases. */ private formatOfferId(offer: PlanPrice) { - return `${offer.access.type === 'subscription' ? 'S' : 'C'}${offer.id}`; //`${offer.access.type === 'subscription' ? 'S' : 'C'}S${offer.item_id}_${offer.id}`; + return `${offer.access.type === 'subscription' ? 'S' : 'C'}${offer.id}`; } private formatOffer = ({ title, planId, planOriginalId, ...offer }: PlanPrice & { title: string; planId: string; planOriginalId: number }): Offer => { From a8aa86506e4b2538e24148408452596c2839a4a2 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 13:00:03 +0200 Subject: [PATCH 30/44] fix: add comment about 'any' --- packages/ui-react/src/components/Button/Button.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui-react/src/components/Button/Button.tsx b/packages/ui-react/src/components/Button/Button.tsx index 5d89f72c8..e82652b2c 100644 --- a/packages/ui-react/src/components/Button/Button.tsx +++ b/packages/ui-react/src/components/Button/Button.tsx @@ -29,6 +29,8 @@ type Props = { busy?: boolean; id?: string; activeClassname?: string; + + // we are using 'any' here because this prop is mapped to NavLink's 'state' prop, whose type is also 'any' navLinkState?: any; } & React.AriaAttributes; From b4fa729b34176eb43b91e92d2712f9a784645399 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 14:25:43 +0200 Subject: [PATCH 31/44] fix: remove obsolete workaround --- packages/hooks-react/src/usePlansForMedia.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/hooks-react/src/usePlansForMedia.tsx b/packages/hooks-react/src/usePlansForMedia.tsx index a00c580ac..d26cf1009 100644 --- a/packages/hooks-react/src/usePlansForMedia.tsx +++ b/packages/hooks-react/src/usePlansForMedia.tsx @@ -1,10 +1,11 @@ import { useQuery } from 'react-query'; -import { useLayoutEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { getModule } from '@jwp/ott-common/src/modules/container'; import JWPCheckoutService from '@jwp/ott-common/src/services/integrations/jwp/JWPCheckoutService'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import { MediaStatus } from '@jwp/ott-common/src/utils/liveEvent'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import useMedia from './useMedia'; @@ -84,22 +85,14 @@ const extractCustomParameters = (playlistItem: PlaylistItem): { key: string; val .map(([key, value]) => ({ key, value })); }; -export default function usePlansForMedia(_mediaId: string) { - const [mediaId, setMediaId] = useState(_mediaId); +export default function usePlansForMedia(mediaId: string) { + const accessMethod = useCheckoutStore(({ accessMethod }) => accessMethod); const checkoutController = getModule(JWPCheckoutService); const siteId = useConfigStore(({ config }) => config.siteId); - const { isLoading: isMediaLoading, data: mediaData } = useMedia(mediaId, !!checkoutController); - - useLayoutEffect(() => { - if (_mediaId && _mediaId !== mediaId) { - setMediaId(_mediaId); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [_mediaId]); + const { isLoading: isMediaLoading, data: mediaData } = useMedia(mediaId, accessMethod === 'plan'); const { isLoading: isPlansLoading, data } = useQuery({ queryKey: ['plans', mediaData?.mediaid], From ae8d4ec6f2f10518dba5ea9f99cfe5b7372a492d Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 15:18:26 +0200 Subject: [PATCH 32/44] fix: move invalidation into existing dedicated hook --- packages/hooks-react/src/usePlansForMedia.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/hooks-react/src/usePlansForMedia.tsx b/packages/hooks-react/src/usePlansForMedia.tsx index d26cf1009..4b31375d0 100644 --- a/packages/hooks-react/src/usePlansForMedia.tsx +++ b/packages/hooks-react/src/usePlansForMedia.tsx @@ -1,5 +1,5 @@ -import { useQuery } from 'react-query'; -import { useMemo } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { useEffect, useMemo } from 'react'; import { getModule } from '@jwp/ott-common/src/modules/container'; import JWPCheckoutService from '@jwp/ott-common/src/services/integrations/jwp/JWPCheckoutService'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; @@ -86,6 +86,8 @@ const extractCustomParameters = (playlistItem: PlaylistItem): { key: string; val }; export default function usePlansForMedia(mediaId: string) { + const queryClient = useQueryClient(); + const accessMethod = useCheckoutStore(({ accessMethod }) => accessMethod); const checkoutController = getModule(JWPCheckoutService); @@ -115,5 +117,13 @@ export default function usePlansForMedia(mediaId: string) { const isLoading = isMediaLoading || isPlansLoading; + useEffect( + () => () => { + queryClient.invalidateQueries(['plans', mediaId]); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + return useMemo(() => ({ isLoading, data }), [isLoading, data]); } From 469fc8f42bcf1800505c23d9f3276612c828864f Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 15:19:17 +0200 Subject: [PATCH 33/44] fix: remove invalidation from component --- .../ui-react/src/components/ListPlans/ListPlans.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/ui-react/src/components/ListPlans/ListPlans.tsx b/packages/ui-react/src/components/ListPlans/ListPlans.tsx index 539c6f524..09d739010 100644 --- a/packages/ui-react/src/components/ListPlans/ListPlans.tsx +++ b/packages/ui-react/src/components/ListPlans/ListPlans.tsx @@ -1,7 +1,6 @@ -import React, { useLayoutEffect } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useMatch } from 'react-router'; -import { useQueryClient } from 'react-query'; import usePlansForMedia from '@jwp/ott-hooks-react/src/usePlansForMedia'; import LoadingOverlay from '@jwp/ott-ui-react/src/components/LoadingOverlay/LoadingOverlay'; import { PATH_MEDIA, PATH_USER_PAYMENTS } from '@jwp/ott-common/src/paths'; @@ -14,18 +13,11 @@ import styles from './ListPlans.module.scss'; const ListPlans: React.FC = () => { const { t } = useTranslation('account'); - const queryClient = useQueryClient(); const match = useMatch(PATH_MEDIA); const location = useLocation(); const mediaId = match?.params.id || ''; - useLayoutEffect(() => { - if (mediaId) { - queryClient.invalidateQueries(['plans', mediaId]); - } - }, [mediaId, queryClient]); - const { isLoading, data: plans } = usePlansForMedia(mediaId); if (isLoading) { From 14d8ee51ef82b8b2b9a92aa90e246936840b3161 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 15:19:57 +0200 Subject: [PATCH 34/44] fix: only display plans that have prices --- .../common/src/services/integrations/jwp/JWPCheckoutService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 8c828e4ad..e29d22641 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -150,7 +150,7 @@ export default class JWPCheckoutService extends CheckoutService { }), ); - return offers; + return offers.filter(([, offers]) => offers.length > 0); } catch { throw new Error('Failed to get plans'); } From 8c3101204cb0fa5b37feba8e7415f19a83b5cd58 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski Date: Mon, 24 Jun 2024 16:44:29 +0200 Subject: [PATCH 35/44] fix: eliminate direct usage of jw services --- .../src/controllers/CheckoutController.ts | 4 ++++ packages/common/src/modules/register.ts | 1 - .../services/integrations/CheckoutService.ts | 3 +++ .../cleeng/CleengCheckoutService.ts | 2 ++ .../integrations/jwp/JWPCheckoutService.ts | 6 +++-- packages/common/types/checkout.ts | 1 + packages/hooks-react/src/usePlansForMedia.tsx | 9 ++----- .../ChoosePlanForm/ChoosePlanForm.test.tsx | 6 +++++ .../ChoosePlanForm/ChoosePlanForm.tsx | 24 +++++++++++++++---- .../src/components/ListPlans/ListPlans.tsx | 4 ++-- .../src/components/PlanBox/PlanBox.tsx | 14 +++++------ .../AccountModal/forms/ChooseOffer.tsx | 12 +++++++++- 12 files changed, 62 insertions(+), 24 deletions(-) diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 282b5a493..f4c1e2cba 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -353,6 +353,10 @@ export default class CheckoutController { return this.checkoutService.getOffers(payload); }; + getPlansWithPriceOffers = (searchString: string) => { + return this.checkoutService.getPlansWithPriceOffers(searchString); + }; + getEntitlements: GetEntitlements = (payload) => { return this.checkoutService.getEntitlements(payload); }; diff --git a/packages/common/src/modules/register.ts b/packages/common/src/modules/register.ts index 0c2c8bece..e26533416 100644 --- a/packages/common/src/modules/register.ts +++ b/packages/common/src/modules/register.ts @@ -82,7 +82,6 @@ container.bind(SubscriptionService).to(CleengSubscriptionService).whenTargetName // JWP integration container.bind(DETERMINE_INTEGRATION_TYPE).toConstantValue(isJwpIntegrationType); container.bind(JWPEntitlementService).toSelf(); -container.bind(JWPCheckoutService).toSelf().whenTargetNamed(INTEGRATION.JWP); container.bind(AccountService).to(JWPAccountService).whenTargetNamed(INTEGRATION.JWP); container.bind(CheckoutService).to(JWPCheckoutService).whenTargetNamed(INTEGRATION.JWP); container.bind(SubscriptionService).to(JWPSubscriptionService).whenTargetNamed(INTEGRATION.JWP); diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index daa555e72..8f780a6be 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -11,6 +11,7 @@ import type { GetInitialAdyenPayment, GetOffer, GetOffers, + GetPlansWithPriceOffers, GetOrder, GetPaymentMethods, GetSubscriptionSwitch, @@ -30,6 +31,8 @@ export default abstract class CheckoutService { abstract getOffers: GetOffers; + abstract getPlansWithPriceOffers: GetPlansWithPriceOffers; + abstract createOrder: CreateOrder; abstract updateOrder: UpdateOrder; diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index f2730d753..3d3427ca6 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -60,6 +60,8 @@ export default class CleengCheckoutService extends CheckoutService { ); }; + getPlansWithPriceOffers = async () => []; + getOffer: GetOffer = async (payload) => { const customerIP = await this.getCustomerIP(); diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index e29d22641..d8332305d 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -135,15 +135,17 @@ export default class JWPCheckoutService extends CheckoutService { try { const prices = await this.getPlanPrices(plan.id); + const planProps = { id: plan.id, name: plan.metadata.name }; + if (prices.length) { const offers = prices.map((offer) => this.formatOffer({ ...offer, planId: plan.id, planOriginalId: plan.original_id, title: plan.metadata.name }), ); - return [plan, offers] as const; + return [planProps, offers] as const; } - return [plan, [] as Offer[]] as const; + return [planProps, [] as Offer[]] as const; } catch { throw new Error(); } diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index abbf887ff..5a95a3670 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -369,6 +369,7 @@ export type FinalizeAdyenPaymentDetailsResponse = PaymentDetail; export type GetOffers = PromiseRequest; export type GetOffer = EnvironmentServiceRequest; +export type GetPlansWithPriceOffers = PromiseRequest; export type CreateOrder = EnvironmentServiceRequest; export type GetOrder = EnvironmentServiceRequest; export type UpdateOrder = EnvironmentServiceRequest; diff --git a/packages/hooks-react/src/usePlansForMedia.tsx b/packages/hooks-react/src/usePlansForMedia.tsx index 4b31375d0..10d191da7 100644 --- a/packages/hooks-react/src/usePlansForMedia.tsx +++ b/packages/hooks-react/src/usePlansForMedia.tsx @@ -1,10 +1,9 @@ import { useQuery, useQueryClient } from 'react-query'; import { useEffect, useMemo } from 'react'; import { getModule } from '@jwp/ott-common/src/modules/container'; -import JWPCheckoutService from '@jwp/ott-common/src/services/integrations/jwp/JWPCheckoutService'; +import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import { MediaStatus } from '@jwp/ott-common/src/utils/liveEvent'; -import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useCheckoutStore } from '@jwp/ott-common/src/stores/CheckoutStore'; import useMedia from './useMedia'; @@ -90,9 +89,7 @@ export default function usePlansForMedia(mediaId: string) { const accessMethod = useCheckoutStore(({ accessMethod }) => accessMethod); - const checkoutController = getModule(JWPCheckoutService); - - const siteId = useConfigStore(({ config }) => config.siteId); + const checkoutController = getModule(CheckoutController); const { isLoading: isMediaLoading, data: mediaData } = useMedia(mediaId, accessMethod === 'plan'); @@ -108,8 +105,6 @@ export default function usePlansForMedia(mediaId: string) { const searchString = `q=${buildFilterQuery(tags, customParameters)}`; - checkoutController.siteId = siteId; - return await checkoutController.getPlansWithPriceOffers(searchString); }, enabled: !!checkoutController && !!mediaData, diff --git a/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.test.tsx b/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.test.tsx index e590e1601..d5bca5e97 100644 --- a/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.test.tsx +++ b/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.test.tsx @@ -16,6 +16,7 @@ describe('', () => { values={{ selectedOfferId: 'S916977979_NL', selectedOfferType: 'svod' }} errors={{}} onChange={vi.fn()} + setValue={vi.fn()} onSubmit={vi.fn()} submitting={false} offers={svodOffers} @@ -31,6 +32,7 @@ describe('', () => { values={{ selectedOfferId: 'S916977979_NL', selectedOfferType: 'svod' }} errors={{}} onChange={vi.fn()} + setValue={vi.fn()} onSubmit={vi.fn()} submitting={false} offers={svodOffers} @@ -46,6 +48,7 @@ describe('', () => { values={{ selectedOfferId: 'S345569153_NL', selectedOfferType: 'svod' }} errors={{}} onChange={vi.fn()} + setValue={vi.fn()} onSubmit={vi.fn()} submitting={false} offers={svodOffers} @@ -62,6 +65,7 @@ describe('', () => { values={{ selectedOfferId: 'S916977979_NL', selectedOfferType: 'svod' }} errors={{}} onChange={onChange} + setValue={vi.fn()} onSubmit={vi.fn()} submitting={false} offers={svodOffers} @@ -80,6 +84,7 @@ describe('', () => { values={{ selectedOfferId: 'S916977979_NL', selectedOfferType: 'svod' }} errors={{}} onChange={vi.fn()} + setValue={vi.fn()} onSubmit={onSubmit} submitting={false} offers={svodOffers} @@ -97,6 +102,7 @@ describe('', () => { values={{ selectedOfferId: 'S916977979_NL', selectedOfferType: 'svod' }} errors={{}} onChange={vi.fn()} + setValue={vi.fn()} onSubmit={vi.fn()} submitting={false} offers={svodOffers} diff --git a/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.tsx b/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.tsx index fd54264d0..730842cc0 100644 --- a/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.tsx +++ b/packages/ui-react/src/components/ChoosePlanForm/ChoosePlanForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; import type { FormErrors } from '@jwp/ott-common/types/form'; import type { Offer, ChooseOfferFormData } from '@jwp/ott-common/types/checkout'; @@ -19,13 +19,14 @@ type Props = { values: ChooseOfferFormData; errors: FormErrors; onChange: React.ChangeEventHandler; + setValue: (key: keyof ChooseOfferFormData, value: string) => void; onSubmit: React.FormEventHandler; onBackButtonClickHandler?: () => void; offers: Offer[]; submitting: boolean; }; -const ChoosePlanForm: React.FC = ({ values, errors, submitting, offers, onChange, onSubmit, onBackButtonClickHandler }: Props) => { +const ChoosePlanForm: React.FC = ({ values, errors, submitting, offers, onChange, setValue, onSubmit, onBackButtonClickHandler }: Props) => { const { t } = useTranslation('account'); const { selectedOfferId } = values; @@ -36,6 +37,12 @@ const ChoosePlanForm: React.FC = ({ values, errors, submitting, offers, o const [offerFilter, setOfferFilter] = useState(() => Object.keys(groupedOffers)[0] as OfferPeriod); + useLayoutEffect(() => { + setValue('selectedOfferId', groupedOffers[offerFilter as OfferPeriod][0].offerId); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [offerFilter]); + return (
    {onBackButtonClickHandler ? : null} @@ -47,7 +54,9 @@ const ChoosePlanForm: React.FC = ({ values, errors, submitting, offers, o
    {submitting && } -