From 5c5e3886360ad7e6da01a9401ca98ccc1d71a8f4 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Wed, 18 May 2022 14:57:35 +0200 Subject: [PATCH] feat(entitlement): add tvod entitlement check to movie screen --- .commitlintrc.js | 1 + src/hooks/useEntitlement.ts | 50 +++++++++++++++++++++++++ src/screens/Movie/Movie.tsx | 47 ++++++++++++----------- src/services/checkout.service.ts | 19 ++++++++-- src/utils/cleeng.ts | 13 ++++--- types/checkout.d.ts | 64 ++++++++++++++++++-------------- types/playlist.d.ts | 1 + 7 files changed, 138 insertions(+), 57 deletions(-) create mode 100644 src/hooks/useEntitlement.ts diff --git a/.commitlintrc.js b/.commitlintrc.js index 11d561b44..8127cd3e8 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -22,6 +22,7 @@ module.exports = { 'menu', 'payment', 'e2e', + 'entitlement', ], ], }, diff --git a/src/hooks/useEntitlement.ts b/src/hooks/useEntitlement.ts new file mode 100644 index 000000000..7692b5131 --- /dev/null +++ b/src/hooks/useEntitlement.ts @@ -0,0 +1,50 @@ +import { useQueries } from 'react-query'; +import { useMemo } from 'react'; + +import type { GetEntitlementsResponse } from '../../types/checkout'; + +import { ConfigStore } from '#src/stores/ConfigStore'; +import { AccountStore } from '#src/stores/AccountStore'; +import { getEntitlements } from '#src/services/checkout.service'; + +export type UseEntitlementResult = { + isEntitled: boolean; + isLoading: boolean; + error: unknown | null; +}; + +export type UseEntitlement = (offerIds?: string[], enabled?: boolean) => UseEntitlementResult; + +type QueryResult = { + responseData?: GetEntitlementsResponse; +}; + +const useEntitlement: UseEntitlement = (offerIds = [], enabled = true) => { + const sandbox = ConfigStore.useState(({ config }) => config?.cleengSandbox); + const jwt = AccountStore.useState(({ auth }) => auth?.jwt); + + const entitlementQueries = useQueries( + offerIds.map((offerId) => ({ + queryKey: ['mediaOffer', offerId], + queryFn: () => getEntitlements({ offerId: offerId || '' }, sandbox, jwt || ''), + enabled: enabled && !!jwt && !!offerId, + })), + ); + + const evaluateEntitlement = (queryResult: QueryResult) => !!queryResult?.responseData?.accessGranted; + + return useMemo( + () => + entitlementQueries.reduce( + (prev, cur) => ({ + isLoading: prev.isLoading || cur.isLoading, + error: prev.error || cur.error, + isEntitled: prev.isEntitled || (cur.isSuccess && evaluateEntitlement(cur as QueryResult)), + }), + { isEntitled: false, isLoading: false, error: null }, + ), + [entitlementQueries], + ); +}; + +export default useEntitlement; diff --git a/src/screens/Movie/Movie.tsx b/src/screens/Movie/Movie.tsx index d59a3f61f..532f4572e 100644 --- a/src/screens/Movie/Movie.tsx +++ b/src/screens/Movie/Movie.tsx @@ -4,28 +4,29 @@ import { useHistory } from 'react-router'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import { useFavorites } from '../../stores/FavoritesStore'; -import useBlurImageUpdater from '../../hooks/useBlurImageUpdater'; -import { cardUrl, movieURL, videoUrl } from '../../utils/formatting'; -import type { PlaylistItem } from '../../../types/playlist'; -import VideoComponent from '../../components/Video/Video'; -import ErrorPage from '../../components/ErrorPage/ErrorPage'; -import CardGrid from '../../components/CardGrid/CardGrid'; -import useMedia from '../../hooks/useMedia'; -import { generateMovieJSONLD } from '../../utils/structuredData'; -import { copyToClipboard } from '../../utils/dom'; -import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay'; -import useRecommendedPlaylist from '../../hooks/useRecommendationsPlaylist'; -import { watchHistoryStore } from '../../stores/WatchHistoryStore'; -import { VideoProgressMinMax } from '../../config'; -import { ConfigStore } from '../../stores/ConfigStore'; -import { AccountStore } from '../../stores/AccountStore'; -import { addQueryParam } from '../../utils/history'; -import { isAllowedToWatch } from '../../utils/cleeng'; -import { addConfigParamToUrl } from '../../utils/configOverride'; - import styles from './Movie.module.scss'; +import { useFavorites } from '#src/stores/FavoritesStore'; +import useBlurImageUpdater from '#src/hooks/useBlurImageUpdater'; +import { cardUrl, movieURL, videoUrl } from '#src/utils/formatting'; +import type { PlaylistItem } from '#src/../types/playlist'; +import VideoComponent from '#src/components/Video/Video'; +import ErrorPage from '#src/components/ErrorPage/ErrorPage'; +import CardGrid from '#src/components/CardGrid/CardGrid'; +import useMedia from '#src/hooks/useMedia'; +import { generateMovieJSONLD } from '#src/utils/structuredData'; +import { copyToClipboard } from '#src/utils/dom'; +import LoadingOverlay from '#src/components/LoadingOverlay/LoadingOverlay'; +import useRecommendedPlaylist from '#src/hooks/useRecommendationsPlaylist'; +import { watchHistoryStore } from '#src/stores/WatchHistoryStore'; +import { VideoProgressMinMax } from '#src/config'; +import { ConfigStore } from '#src/stores/ConfigStore'; +import { AccountStore } from '#src/stores/AccountStore'; +import { addQueryParam } from '#src/utils/history'; +import { filterCleengMediaOffers, isAllowedToWatch } from '#src/utils/cleeng'; +import { addConfigParamToUrl } from '#src/utils/configOverride'; +import useEntitlement from '#src/hooks/useEntitlement'; + type MovieRouteParams = { id: string; }; @@ -48,11 +49,15 @@ const Movie = ({ match, location }: RouteComponentProps): JSX. // Media const { isLoading, error, data: item } = useMedia(id); - const itemRequiresSubscription = item?.requiresSubscription !== 'false'; + const itemRequiresSubscription = item?.requiresSubscription !== 'false'; // || item.free ? useBlurImageUpdater(item); const { data: trailerItem } = useMedia(item?.trailerId || ''); const { data: playlist } = useRecommendedPlaylist(recommendationsPlaylist || '', item); + // AccessModel & entitlement + const mediaOffer = useMemo(() => (item?.productIds && filterCleengMediaOffers(item.productIds)) || undefined, [item]); + useEntitlement(mediaOffer); + const { hasItem, saveItem, removeItem } = useFavorites(); const watchHistory = watchHistoryStore.useState((s) => s.watchHistory); diff --git a/src/services/checkout.service.ts b/src/services/checkout.service.ts index b6e550202..a6b072033 100644 --- a/src/services/checkout.service.ts +++ b/src/services/checkout.service.ts @@ -1,8 +1,17 @@ -import type { CreateOrder, GetOffer, GetPaymentMethods, PaymentWithAdyen, PaymentWithoutDetails, PaymentWithPayPal, UpdateOrder } from '../../types/checkout'; -import { getOverrideIP, IS_DEV_BUILD } from '../utils/common'; - import { get, post, patch } from './cleeng.service'; +import type { + CreateOrder, + GetEntitlements, + GetOffer, + GetPaymentMethods, + PaymentWithAdyen, + PaymentWithoutDetails, + PaymentWithPayPal, + UpdateOrder, +} from '#types/checkout'; +import { getOverrideIP, IS_DEV_BUILD } from '#src/utils/common'; + export const getOffer: GetOffer = async (payload, sandbox) => { // @ts-ignore return get(sandbox, `/offers/${payload.offerId}${IS_DEV_BUILD && getOverrideIP() ? '?customerIP=' + getOverrideIP() : ''}`); @@ -35,3 +44,7 @@ export const paymentWithAdyen: PaymentWithAdyen = async (payload, sandbox, jwt) export const paymentWithPayPal: PaymentWithPayPal = async (payload, sandbox, jwt) => { return post(sandbox, '/connectors/paypal/v1/tokens', JSON.stringify(payload), jwt); }; + +export const getEntitlements: GetEntitlements = async (payload, sandbox, jwt = '') => { + return get(sandbox, `/entitlements/${payload.offerId}`, jwt); +}; diff --git a/src/utils/cleeng.ts b/src/utils/cleeng.ts index 539fcb063..16eb5e885 100644 --- a/src/utils/cleeng.ts +++ b/src/utils/cleeng.ts @@ -1,11 +1,12 @@ import type { AccessModel } from '../../types/Config'; -export const isAllowedToWatch = ( - accessModel: AccessModel, - isLoggedIn: boolean, - itemRequiresSubscription: boolean, - hasSubscription: boolean, -): boolean => +export const isAllowedToWatch = (accessModel: AccessModel, isLoggedIn: boolean, itemRequiresSubscription: boolean, hasSubscription: boolean): boolean => accessModel === 'AVOD' || (accessModel === 'AUTHVOD' && (isLoggedIn || !itemRequiresSubscription)) || (accessModel === 'SVOD' && (hasSubscription || !itemRequiresSubscription)); + +export const filterCleengMediaOffers = (productIds: string): string[] => { + return productIds + .split(',') + .reduce((ids, productId) => (productId?.indexOf('cleeng:') === 0 ? [...ids, productId.replace('cleeng:', '')] : ids), []); +}; diff --git a/types/checkout.d.ts b/types/checkout.d.ts index 745d02845..b4fd11082 100644 --- a/types/checkout.d.ts +++ b/types/checkout.d.ts @@ -33,22 +33,22 @@ export type Offer = { videoId: string | null; contentExternalId: string | null; contentExternalData: string | null; - contentAgeRestriction: string | null -} + contentAgeRestriction: string | null; +}; export type OrderOffer = { title: string; description: string | null; price: number; currency: string; -} +}; export type Order = { id: number; customerId: string; customer: { locale: string; - email: string + email: string; }; publisherId: number; offerId: string; @@ -60,7 +60,7 @@ export type Order = { discountedPrice: number; taxValue: number; customerServiceFee: number; - paymentMethodFee: number + paymentMethodFee: number; }; taxRate: number; taxBreakdown: string | null; @@ -73,9 +73,9 @@ export type Order = { discount: { applied: boolean; type: string; - periods: number + periods: number; }; - requiredPaymentDetails: boolean + requiredPaymentDetails: boolean; }; export type PaymentMethod = { @@ -91,26 +91,26 @@ export type PaymentMethodResponse = { }; export type Payment = { - id: number, - orderId: number, - status: string, - totalAmount: number, - currency: string, - customerId: string, - paymentGateway: string, - paymentMethod: string, - externalPaymentId: string|number, - couponId: number | null, - amount: number, - country: string, - offerType: "subscription", - taxValue: number, - paymentMethodFee: number, - customerServiceFee: number, - rejectedReason: string | null, - refundedReason: string | null, - paymentDetailsId: number | null, - paymentOperation: string + id: number; + orderId: number; + status: string; + totalAmount: number; + currency: string; + customerId: string; + paymentGateway: string; + paymentMethod: string; + externalPaymentId: string | number; + couponId: number | null; + amount: number; + country: string; + offerType: 'subscription'; + taxValue: number; + paymentMethodFee: number; + customerServiceFee: number; + rejectedReason: string | null; + refundedReason: string | null; + paymentDetailsId: number | null; + paymentOperation: string; }; export type GetOfferPayload = { @@ -165,6 +165,15 @@ export type PaymentWithPayPalResponse = { redirectUrl: string; }; +export type GetEntitlementsPayload = { + offerId: string; +}; + +export type GetEntitlementsResponse = { + accessGranted: boolean; + expiresAt: number; +}; + export type GetOffer = CleengRequest; export type CreateOrder = CleengAuthRequest; export type UpdateOrder = CleengAuthRequest; @@ -172,3 +181,4 @@ export type GetPaymentMethods = CleengEmptyAuthRequest; export type PaymentWithoutDetails = CleengAuthRequest; export type PaymentWithAdyen = CleengAuthRequest; export type PaymentWithPayPal = CleengAuthRequest; +export type GetEntitlements = CleengAuthRequest; diff --git a/types/playlist.d.ts b/types/playlist.d.ts index 88ec4a2b1..709fc8ab4 100644 --- a/types/playlist.d.ts +++ b/types/playlist.d.ts @@ -38,6 +38,7 @@ export type PlaylistItem = { title: string; tracks: Track[]; variations?: Record; + productIds?: string; }; export type Link = {