From e993e7c228b65f4ec716ba78123f5499cbe7495d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Tue, 3 Sep 2024 16:53:44 +0200 Subject: [PATCH 01/12] feat(project): add refresh passport mechanism --- .../src/controllers/AccessController.ts | 29 +++++++++++++++---- packages/common/src/services/AccessService.ts | 29 ++++++++++++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/common/src/controllers/AccessController.ts b/packages/common/src/controllers/AccessController.ts index a6f1c1a37..695f03ccb 100644 --- a/packages/common/src/controllers/AccessController.ts +++ b/packages/common/src/controllers/AccessController.ts @@ -33,16 +33,20 @@ export default class AccessController { // Not awaiting to avoid blocking the loading process, // as the passport can be stored asynchronously without affecting the app's performance - this.generateOrRetrievePassport(); + this.generateOrRefreshPassport(); }; - generateOrRetrievePassport = async () => { + generateOrRefreshPassport = async () => { if (!this.apiAccessBridgeUrl) { return; } const existingPassport = await this.accessService.getPassport(); - if (existingPassport) { + const shouldRefresh = existingPassport && Date.now() > existingPassport.expires; + + if (shouldRefresh) { + return await this.refreshPassport(); + } else if (existingPassport) { return existingPassport; } @@ -58,10 +62,25 @@ export default class AccessController { return; } - const { config } = useConfigStore.getState(); const auth = await this.accountService.getAuthData(); - const passport = await this.accessService.generatePassport(this.apiAccessBridgeUrl, config.siteId, auth?.jwt); + const passport = await this.accessService.generatePassport(this.apiAccessBridgeUrl, this.siteId, auth?.jwt); + if (passport) { + await this.accessService.setPassport(passport); + } + }; + + refreshPassport = async () => { + if (!this.apiAccessBridgeUrl) { + return; + } + + const existingPassport = await this.accessService.getPassport(); + if (!existingPassport) { + return; + } + + const passport = await this.accessService.refreshPassport(this.apiAccessBridgeUrl, this.siteId, existingPassport.refresh_token); if (passport) { await this.accessService.setPassport(passport); } diff --git a/packages/common/src/services/AccessService.ts b/packages/common/src/services/AccessService.ts index f7a2cf157..ee98247fe 100644 --- a/packages/common/src/services/AccessService.ts +++ b/packages/common/src/services/AccessService.ts @@ -6,6 +6,8 @@ import StorageService from './StorageService'; const PASSPORT_KEY = 'passport'; +type PassportStorageData = Passport & { expires: number }; + @injectable() export default class AccessService { private readonly storageService: StorageService; @@ -36,12 +38,37 @@ export default class AccessService { return (await response.json()) as Passport; }; + refreshPassport = async (host: string, siteId: string, refresh_token: string): Promise => { + if (!siteId) { + throw new Error('Site ID is required'); + } + + const pathname = `/v2/sites/${siteId}/access/refresh`; + const url = `${host}${pathname}`; + + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refresh_token, + }), + }); + + if (!response.ok) { + return null; + } + + return (await response.json()) as Passport; + }; + setPassport = (passport: Passport) => { const expires = new Date(Date.now() + 3600 * 1000).getTime(); // Set expiration time to one hour from now return this.storageService.setItem(PASSPORT_KEY, JSON.stringify({ ...passport, expires }), true); }; - getPassport = async (): Promise<(Passport & { expires: string }) | null> => { + getPassport = async (): Promise => { return await this.storageService.getItem(PASSPORT_KEY, true, true); }; From d224273c0de0e701da660fbe221decc66017559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Thu, 5 Sep 2024 09:51:37 +0200 Subject: [PATCH 02/12] feat(project): add getEntitledPlans service --- packages/common/src/env.ts | 2 +- packages/common/src/services/AccessService.ts | 4 +++ .../src/services/JWPEntitlementService.ts | 14 ++++++++ .../integrations/jwp/JWPAPIService.ts | 2 +- packages/common/types/checkout.ts | 2 ++ packages/common/types/plans.ts | 35 +++++++++++++++++++ packages/hooks-react/src/useProtectedMedia.ts | 1 - platforms/web/.env | 2 +- 8 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 packages/common/types/plans.ts diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index a791fea8e..be18d8ac1 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://cdn.jwplayer.com', + APP_API_BASE_URL: 'https://cdn-dev.jwplayer.com', APP_API_ACCESS_BRIDGE_URL: '', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', diff --git a/packages/common/src/services/AccessService.ts b/packages/common/src/services/AccessService.ts index ee98247fe..46db22af8 100644 --- a/packages/common/src/services/AccessService.ts +++ b/packages/common/src/services/AccessService.ts @@ -16,6 +16,10 @@ export default class AccessService { this.storageService = storageService; } + // getProtectedMediaById = async (mediaId: string) => { + + // } + generatePassport = async (host: string, siteId: string, jwt?: string): Promise => { if (!siteId) { throw new Error('Site ID is required'); diff --git a/packages/common/src/services/JWPEntitlementService.ts b/packages/common/src/services/JWPEntitlementService.ts index b121f91e4..fec7e51be 100644 --- a/packages/common/src/services/JWPEntitlementService.ts +++ b/packages/common/src/services/JWPEntitlementService.ts @@ -1,5 +1,8 @@ import { inject, injectable } from 'inversify'; +import type { GetEntitledPlans } from '../../types/checkout'; +import type { PlansResponse } from '../../types/plans'; + import type { SignedMediaResponse } from './integrations/jwp/types'; import JWPAPIService from './integrations/jwp/JWPAPIService'; @@ -29,4 +32,15 @@ export default class JWPEntitlementService { throw new Error('Unauthorized'); } }; + + getEntitledPlans: GetEntitledPlans = async ({ siteId }) => { + try { + const data = await this.apiService.get(`/v3/sites/${siteId}/entitlements`, { + withAuthentication: await this.apiService.isAuthenticated(), + }); + return data; + } catch { + throw new Error('Failed to get entitled plans'); + } + }; } diff --git a/packages/common/src/services/integrations/jwp/JWPAPIService.ts b/packages/common/src/services/integrations/jwp/JWPAPIService.ts index ebff03a22..cb36ed96d 100644 --- a/packages/common/src/services/integrations/jwp/JWPAPIService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAPIService.ts @@ -34,7 +34,7 @@ export default class JWPAPIService { this.useSandboxEnv = useSandboxEnv; }; - private getBaseUrl = () => (this.useSandboxEnv ? 'https://staging-sims.jwplayer.com' : 'https://sims.jwplayer.com'); + private getBaseUrl = () => (this.useSandboxEnv ? 'https://daily-sims.jwplayer.com' : 'https://sims.jwplayer.com'); setToken = (token: string, refreshToken = '', expires: number) => { return this.storageService.setItem(INPLAYER_TOKEN_KEY, JSON.stringify({ token, refreshToken, expires }), false); diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index 89210f117..584dfc1eb 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -1,6 +1,7 @@ import type { PayloadWithIPOverride } from './account'; import type { PaymentDetail } from './subscription'; import type { EmptyEnvironmentServiceRequest, EnvironmentServiceRequest, PromiseRequest } from './service'; +import type { PlansResponse } from './plans'; export type Offer = { id: number | null; @@ -384,3 +385,4 @@ export type DeletePaymentMethod = EnvironmentServiceRequest; export type FinalizeAdyenPaymentDetails = EnvironmentServiceRequest; export type GetDirectPostCardPayment = (cardPaymentPayload: CardPaymentData, order: Order, referrer: string, returnUrl: string) => Promise; +export type GetEntitledPlans = PromiseRequest<{ siteId: string }, PlansResponse>; diff --git a/packages/common/types/plans.ts b/packages/common/types/plans.ts new file mode 100644 index 000000000..fff88440c --- /dev/null +++ b/packages/common/types/plans.ts @@ -0,0 +1,35 @@ +type AccessOptions = { + drm_policy_id: string; + include_tags: string[] | null; + exclude_tags: string[] | null; + include_custom_params: string[] | null; + exclude_custom_params: string[] | null; +}; + +type PlanExternalProviders = { + stripe?: string; + apple?: string; + google?: string; +}; + +export type AccessControlPlan = { + id: string; + exp: number; +}; + +export type Plan = { + name: string; + access_model: 'free' | 'freeauth' | 'svod'; + access_plan: AccessControlPlan; + access: AccessOptions; + metadata: { + external_providers: PlanExternalProviders; + }; +}; + +export type PlansResponse = { + total: number; + page: number; + page_length: number; + plans: Plan[]; +}; diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index c75649b95..eeb0e4a4a 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -8,7 +8,6 @@ import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => apiService.getMediaById(item.mediaid, token, drmPolicyId)); - const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], () => { diff --git a/platforms/web/.env b/platforms/web/.env index fb9ab08f5..757cb9f38 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://cdn-dev.jwplayer.com APP_PLAYER_ID=M4qoGvUk # page metadata (SEO) From 20ddad5636aafcb516fb0258bdda1a51543a45b4 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Mon, 16 Sep 2024 14:02:58 +0200 Subject: [PATCH 03/12] chore: maintenance 2024 q3 (#608) * chore(project): remove obsolete dep resolutions * chore(project): add resolutions for high risk dep issues * chore(project): add resolution for medium risk dep issues * chore(project): update date-fns * chore(project): update dompurify * chore(project): update fast-xml-parser * chore(project): update zustand * chore(project): perform yarn upgrade for payment deps * chore(project): perform yarn upgrade (interactive) * chore(project): sync yarn lock * refactor(project): fix incorrect store update * refactor(project): update yarn lock * refactor(project): remove deprecated zustand State type * chore(project): update syncpack rules for non-peer local packages --- .syncpackrc.json | 82 +-- configs/eslint-config-jwp/package.json | 2 +- package.json | 14 +- packages/common/package.json | 6 +- .../common/src/controllers/AppController.ts | 6 +- packages/common/src/stores/utils.ts | 6 +- packages/common/src/utils/compare.ts | 2 +- packages/hooks-react/package.json | 2 +- packages/ui-react/package.json | 12 +- platforms/web/package.json | 8 +- yarn.lock | 662 +++++++----------- 11 files changed, 300 insertions(+), 502 deletions(-) diff --git a/.syncpackrc.json b/.syncpackrc.json index 5de916305..118467bd6 100644 --- a/.syncpackrc.json +++ b/.syncpackrc.json @@ -1,88 +1,38 @@ { "$schema": "https://unpkg.com/syncpack@11.2.1/dist/schema.json", - "sortFirst": [ - "name", - "description", - "version", - "private", - "license", - "repository", - "author", - "main", - "exports", - "engines", - "workspaces", - "scripts" - ], + "sortFirst": ["name", "description", "version", "private", "license", "repository", "author", "main", "exports", "engines", "workspaces", "scripts"], "semverGroups": [ { - "dependencies": [ - "codeceptjs", - "codeceptjs**", - "react-router", - "react-router-dom", - "typescript" - ], - "packages": [ - "**" - ], + "dependencies": ["codeceptjs", "codeceptjs**", "react-router", "react-router-dom", "typescript"], + "packages": ["**"], "isIgnored": true }, { "range": "^", - "dependencies": [ - "**" - ], - "packages": [ - "**" - ], - "dependencyTypes": [ - "prod", - "dev", - "peer" - ] + "dependencies": ["**"], + "packages": ["**"], + "dependencyTypes": ["prod", "dev", "peer"] } ], "versionGroups": [ { "label": "Ensure semver ranges for locally developed packages satisfy the local version", - "dependencies": [ - "@jwp/**", - "**-config-jwp" - ], - "dependencyTypes": [ - "peer" - ], - "packages": [ - "**" - ], + "dependencies": ["@jwp/**", "**-config-jwp"], + "dependencyTypes": ["dev", "prod", "peer"], + "packages": ["**"], "pinVersion": "*" }, { - "label": "Ensure local packages are installed as peerDependency", - "dependencies": [ - "@jwp/**", - "**-config-jwp" - ], - "dependencyTypes": [ - "dev", - "prod" - ], - "packages": [ - "**" - ], + "label": "Ensure local packages are installed as dev or prod dependency", + "dependencies": ["@jwp/**", "**-config-jwp"], + "dependencyTypes": ["peer"], + "packages": ["**"], "isBanned": true }, { - "dependencies": [ - "@types/**" - ], - "dependencyTypes": [ - "!dev" - ], - "packages": [ - "**" - ], + "dependencies": ["@types/**"], + "dependencyTypes": ["!dev"], + "packages": ["**"], "isBanned": true, "label": "@types packages should only be under devDependencies" } diff --git a/configs/eslint-config-jwp/package.json b/configs/eslint-config-jwp/package.json index 557909bb0..6ae6258e3 100644 --- a/configs/eslint-config-jwp/package.json +++ b/configs/eslint-config-jwp/package.json @@ -11,7 +11,7 @@ "@typescript-eslint/parser": "^7.13.1", "confusing-browser-globals": "^1.0.11", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.34.2", + "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2" } } diff --git a/package.json b/package.json index 3d0ce78c5..c92bd4ca6 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "devDependencies": { "@commitlint/cli": "^17.8.1", "@commitlint/config-conventional": "^17.8.1", - "@types/node": "^18.19.36", + "@types/node": "^18.19.37", "csv-parse": "^5.5.6", "eslint": "^8.57.0", "husky": "^6.0.0", "i18next-parser-workspaces": "^0.2.0", - "knip": "^5.21.1", - "lint-staged": "^15.2.7", + "knip": "^5.30.1", + "lint-staged": "^15.2.10", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "read": "^2.1.0", @@ -56,10 +56,8 @@ "eslint-config-jwp": "*" }, "resolutions": { - "codeceptjs/**/ansi-regex": "^4.1.1", - "codeceptjs/**/minimatch": "^3.0.5", - "flat": "^5.0.1", - "glob-parent": "^5.1.2", - "json5": "^2.2.2" + "codeceptjs/**/fast-xml-parser": "^4.5.0", + "micromatch": ">=4.0.8", + "ws": ">=5.2.4" } } diff --git a/packages/common/package.json b/packages/common/package.json index d93c32b25..784d8029e 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -11,8 +11,8 @@ "dependencies": { "@inplayer-org/inplayer.js": "^3.13.28", "broadcast-channel": "^7.0.0", - "date-fns": "^2.30.0", - "fast-xml-parser": "^4.4.0", + "date-fns": "^3.6.0", + "fast-xml-parser": "^4.5.0", "i18next": "^22.5.1", "ini": "^3.0.1", "inversify": "^6.0.2", @@ -21,7 +21,7 @@ "react-i18next": "^12.3.1", "reflect-metadata": "^0.2.2", "yup": "^0.32.11", - "zustand": "^3.7.2" + "zustand": "^4.5.5" }, "devDependencies": { "@types/ini": "^1.3.34", diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index e750f5c67..6d86d6d36 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -51,11 +51,7 @@ export default class AppController { } // Store the logo right away and set css variables so the error page will be branded - const banner = config.assets.banner; - - useConfigStore.setState((s) => { - s.config.assets.banner = banner; - }); + useConfigStore.setState((state) => merge({}, state, { config: { assets: { banner: config.assets.banner } } })); config = await this.configService.validateConfig(config); config = merge({}, defaultConfig, config); diff --git a/packages/common/src/stores/utils.ts b/packages/common/src/stores/utils.ts index 63b1b9dca..cf664f60b 100644 --- a/packages/common/src/stores/utils.ts +++ b/packages/common/src/stores/utils.ts @@ -1,10 +1,10 @@ -import type { State, StateCreator } from 'zustand'; -import create from 'zustand'; +import type { StateCreator } from 'zustand'; +import { create } from 'zustand'; import { devtools, subscribeWithSelector } from 'zustand/middleware'; import { IS_DEVELOPMENT_BUILD, IS_TEST_MODE } from '../utils/common'; -export const createStore = (name: string, storeFn: StateCreator) => { +export const createStore = (name: string, storeFn: StateCreator) => { const store = subscribeWithSelector(storeFn); // https://github.com/pmndrs/zustand/issues/852#issuecomment-1059783350 diff --git a/packages/common/src/utils/compare.ts b/packages/common/src/utils/compare.ts index 84782b654..c08793f8a 100644 --- a/packages/common/src/utils/compare.ts +++ b/packages/common/src/utils/compare.ts @@ -1,3 +1,3 @@ -import shallow from 'zustand/shallow'; +import { shallow } from 'zustand/shallow'; export { shallow }; diff --git a/packages/hooks-react/package.json b/packages/hooks-react/package.json index f6654f020..ff6d7af68 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": { - "date-fns": "^2.30.0", + "date-fns": "^3.6.0", "i18next": "^22.5.1", "planby": "^0.3.0", "react": "^18.3.1", diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 547c4eda2..28d1e1c42 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -9,15 +9,15 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@adyen/adyen-web": "^5.66.1", + "@adyen/adyen-web": "^5.68.1", "@videodock/tile-slider": "^2.0.0", "classnames": "^2.5.1", - "date-fns": "^2.30.0", - "dompurify": "^2.5.5", + "date-fns": "^3.6.0", + "dompurify": "^3.1.6", "i18next": "^22.5.1", "inversify": "^6.0.2", "marked": "^4.3.0", - "payment": "^2.4.6", + "payment": "^2.4.7", "planby": "^0.3.0", "react": "^18.3.1", "react-app-polyfill": "^3.0.0", @@ -34,7 +34,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^14.3.1", - "@types/dompurify": "^2.4.0", + "@types/dompurify": "^3.0.5", "@types/jwplayer": "^8.31.1", "@types/marked": "^4.3.2", "@types/payment": "^2.1.7", @@ -47,7 +47,7 @@ "vite-plugin-svgr": "^4.2.0", "vitest": "^1.6.0", "vitest-axe": "^1.0.0-pre.3", - "wicg-inert": "^3.1.2" + "wicg-inert": "^3.1.3" }, "peerDependencies": { "@jwp/ott-common": "*", diff --git a/platforms/web/package.json b/platforms/web/package.json index 4582db208..b78345434 100644 --- a/platforms/web/package.json +++ b/platforms/web/package.json @@ -34,14 +34,14 @@ "@jwp/ott-ui-react": "*", "i18next": "^22.5.1", "i18next-browser-languagedetector": "^6.1.8", - "i18next-http-backend": "^2.5.2", + "i18next-http-backend": "^2.6.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", "react-i18next": "^12.3.1", "react-router": "6.14.2", "react-router-dom": "6.14.2", - "wicg-inert": "^3.1.2" + "wicg-inert": "^3.1.3" }, "devDependencies": { "@babel/core": "^7.24.7", @@ -50,7 +50,7 @@ "@testing-library/jest-dom": "^6.4.6", "@types/jwplayer": "^8.31.1", "@types/luxon": "^3.4.2", - "@types/node": "^18.19.36", + "@types/node": "^18.19.37", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", @@ -66,7 +66,7 @@ "i18next-parser": "^8.13.0", "jsdom": "^22.1.0", "luxon": "^3.4.4", - "playwright": "^1.44.5", + "playwright": "^1.45.0", "postcss": "^8.4.38", "postcss-config-jwp": "*", "react-app-polyfill": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 66478c708..2671a7a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,15 +12,15 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== -"@adyen/adyen-web@^5.66.1": - version "5.66.1" - resolved "https://registry.yarnpkg.com/@adyen/adyen-web/-/adyen-web-5.66.1.tgz#0da141f138487e4901f61ee6b9761281f0f91fd4" - integrity sha512-VAXK4hZrK5YyHQC/fLqBe/iArwmJj8KMfLD67s6rLPpsBKvKFGglJNB7CkoBUjN0KLCQ/1rSx+IlBnC33sJIxw== +"@adyen/adyen-web@^5.68.1": + version "5.68.1" + resolved "https://registry.yarnpkg.com/@adyen/adyen-web/-/adyen-web-5.68.1.tgz#f19a30f524fdbb1f0252b5ea77c6a12c14af630a" + integrity sha512-+dHmXOJVKPXrHi9Afq02Hqdnis+e2abQSVRkg5HmS17UqiJ9zTLlQdbEkDCakana4BH7M1ZrwfbUkFUgO0NL5w== dependencies: "@babel/runtime" "^7.15.4" "@babel/runtime-corejs3" "^7.20.1" "@types/applepayjs" "14.0.6" - "@types/googlepay" "^0.7.0" + "@types/googlepay" "0.7.6" classnames "^2.3.1" core-js-pure "^3.25.3" preact "10.13.2" @@ -1167,9 +1167,9 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs3@^7.20.1": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.25.6.tgz#5e3facf42775cc95bcde95746e940061931286e4" + integrity sha512-Gz0Nrobx8szge6kQQ5Z5MX9L3ObqNwCQY1PSwSNzreFL7aHGxv8Fp2j3ETV6/wWdbiV+mW6OSm8oQhg3Tcsniw== dependencies: core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" @@ -1181,13 +1181,20 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.15.4": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" + integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.0.0", "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -1606,29 +1613,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== -"@ericcornelissen/bash-parser@0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@ericcornelissen/bash-parser/-/bash-parser-0.5.3.tgz#cda9f0e9ed3bcf62c29c277de778726425e03b0a" - integrity sha512-9Z0sGuXqf6En19qmwB0Syi1Mc8TYl756dNuuaYal9mrypKa0Jq/IX6aJfh6Rk2S3z66KBisWTqloDo7weYj4zg== - dependencies: - array-last "^1.1.1" - babylon "^6.9.1" - compose-function "^3.0.3" - filter-obj "^1.1.0" - has-own-property "^0.1.0" - identity-function "^1.0.0" - is-iterable "^1.1.0" - iterable-lookahead "^1.0.0" - lodash.curry "^4.1.1" - magic-string "^0.16.0" - map-obj "^2.0.0" - object-pairs "^0.1.0" - object-values "^1.0.0" - reverse-arguments "^1.0.0" - shell-quote-word "^1.0.1" - to-pascal-case "^1.0.0" - unescape-js "^1.0.5" - "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -2240,33 +2224,12 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.scandir@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-3.0.0.tgz#91c0a33e1aeaedcd4bab2bf31be5d1962a55d2a7" - integrity sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg== - dependencies: - "@nodelib/fs.stat" "3.0.0" - run-parallel "^1.2.0" - "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.stat@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-3.0.0.tgz#ef6c829f2b05f42595d88854ebd777d4335ff0a9" - integrity sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ== - -"@nodelib/fs.walk@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-2.0.0.tgz#10499ac2210f6399770b465ba728adafc7d44bb1" - integrity sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A== - dependencies: - "@nodelib/fs.scandir" "3.0.0" - fastq "^1.15.0" - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@1.2.8", "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -3103,10 +3066,10 @@ dependencies: "@babel/types" "^7.20.7" -"@types/dompurify@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" - integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== dependencies: "@types/trusted-types" "*" @@ -3128,7 +3091,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/googlepay@^0.7.0": +"@types/googlepay@0.7.6": version "0.7.6" resolved "https://registry.yarnpkg.com/@types/googlepay/-/googlepay-0.7.6.tgz#ba444ad8b2945e70f873673b8f5371745b8cfe37" integrity sha512-5003wG+qvf4Ktf1hC9IJuRakNzQov00+Xf09pAWGJLpdOjUrq0SSLCpXX7pwSeTG9r5hrdzq1iFyZcW7WVyr4g== @@ -3216,10 +3179,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== -"@types/node@^18.19.36": - version "18.19.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.37.tgz#506ee89d6b5edd5a4c4e01b22162dd8309718a35" - integrity sha512-Pi53fdVMk7Ig5IfAMltQQMgtY7xLzHaEous8IQasYsdQbYK3v90FkxI3XYQCe/Qme58pqp14lXJIsFmGP8VoZQ== +"@types/node@^18.19.37": + version "18.19.50" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.50.tgz#8652b34ee7c0e7e2004b3f08192281808d41bf5a" + integrity sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg== dependencies: undici-types "~5.26.4" @@ -3734,10 +3697,12 @@ ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" - integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" ansi-fragments@^0.2.1: version "0.2.1" @@ -3753,7 +3718,7 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== -ansi-regex@^4.1.0, ansi-regex@^4.1.1: +ansi-regex@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== @@ -3764,9 +3729,9 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" @@ -3836,11 +3801,6 @@ aria-query@^5.0.0: dependencies: dequal "^2.0.3" -arity-n@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" - integrity sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ== - array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" @@ -3866,13 +3826,6 @@ array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: get-intrinsic "^1.2.4" is-string "^1.0.7" -array-last@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" - integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== - dependencies: - is-number "^4.0.0" - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -3934,16 +3887,6 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - array.prototype.tosorted@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" @@ -4018,11 +3961,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async@^3.2.3, async@^3.2.4: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -4133,11 +4071,6 @@ babel-plugin-transform-typescript-metadata@^0.3.2: dependencies: "@babel/helper-plugin-utils" "^7.0.0" -babylon@^6.9.1: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -4791,12 +4724,12 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== dependencies: - restore-cursor "^4.0.0" + restore-cursor "^5.0.0" cli-spinners@^2.5.0: version "2.9.2" @@ -5054,13 +4987,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compose-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" - integrity sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg== - dependencies: - arity-n "^1.0.4" - compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -5210,9 +5136,9 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: browserslist "^4.23.0" core-js-pure@^3.25.3, core-js-pure@^3.30.2: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" - integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.38.1.tgz#e8534062a54b7221344884ba9b52474be495ada3" + integrity sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ== core-js@^3.19.2, core-js@^3.37.1: version "3.37.1" @@ -5460,13 +5386,18 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@^2.28.0, date-fns@^2.30.0: +date-fns@^2.28.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== dependencies: "@babel/runtime" "^7.21.0" +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + dayjs@^1.8.15: version "1.11.11" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" @@ -5484,7 +5415,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.5, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.4: +debug@4, debug@4.3.5, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -5505,6 +5436,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -5654,7 +5592,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -5887,10 +5825,10 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.5.tgz#0540a05b8020d4691ee9c6083fb23b2c919276fc" - integrity sha512-FgbqnEPiv5Vdtwt6Mxl7XSylttCC03cqP5ldNT2z+Kj0nLxPHJH4+1Cyf5Jasxhw93Rl4Oo11qRoUV72fmya2Q== +dompurify@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2" + integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ== domutils@^2.8.0: version "2.8.0" @@ -6046,9 +5984,9 @@ email-addresses@^5.0.0: integrity sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw== emoji-regex@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" - integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== emoji-regex@^8.0.0: version "8.0.0" @@ -6072,6 +6010,14 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.6: version "2.4.1" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" @@ -6105,6 +6051,11 @@ envinfo@^7.10.0: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + eol@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" @@ -6144,7 +6095,7 @@ errorhandler@^1.5.1: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -6420,29 +6371,29 @@ eslint-plugin-react-hooks@^4.6.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@^7.34.2: - version "7.34.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" - integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== +eslint-plugin-react@^7.34.3: + version "7.35.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz#d32500d3ec268656d5071918bfec78cfd8b070ed" + integrity sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.hasown "^1.1.4" object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-scope@^7.2.2: version "7.2.2" @@ -6677,10 +6628,10 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@^4.0.12, fast-xml-parser@^4.2.4, fast-xml-parser@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" - integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== +fast-xml-parser@^4.0.12, fast-xml-parser@^4.2.4, fast-xml-parser@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37" + integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg== dependencies: strnum "^1.0.5" @@ -6689,7 +6640,7 @@ fastest-levenshtein@^1.0.16: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== -fastq@^1.13.0, fastq@^1.15.0, fastq@^1.6.0: +fastq@^1.13.0, fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== @@ -6862,7 +6813,7 @@ flat-cache@^3.0.4, flat-cache@^3.2.0: keyv "^4.5.3" rimraf "^3.0.2" -flat@^5.0.1, flat@^5.0.2: +flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== @@ -7183,13 +7134,20 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@^6.0.2, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-stream@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-8.0.2.tgz#09e5818e41c16dd85274d72c7a7158d307426313" @@ -7296,7 +7254,7 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.2, globalthis@^1.0.3: +globalthis@^1.0.3, globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -7376,11 +7334,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-own-property@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-own-property/-/has-own-property-0.1.0.tgz#992b0f5bb3a25416f8d4d0cde53f497b9d7b1ea5" - integrity sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw== - has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -7629,10 +7582,10 @@ i18next-browser-languagedetector@^6.1.8: dependencies: "@babel/runtime" "^7.19.0" -i18next-http-backend@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz#3d846cc239987fe7700d1cf0f17975807bfd25d3" - integrity sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw== +i18next-http-backend@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz#186c3a1359e10245c9119a13129f9b5bf328c9a7" + integrity sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog== dependencies: cross-fetch "4.0.0" @@ -7714,17 +7667,17 @@ idb@^7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== -identity-function@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" - integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== - ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.1.8, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: +ignore@^5.1.8: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -7940,7 +7893,14 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0: +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-core-module@^2.13.1, is-core-module@^2.5.0: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== @@ -8024,11 +7984,6 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-iterable@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-iterable/-/is-iterable-1.1.1.tgz#71f9aa6f113e1d968ebe1d41cff4c8fb23a817bc" - integrity sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ== - is-map@^2.0.2, is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" @@ -8056,11 +8011,6 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -8284,11 +8234,6 @@ istanbul-reports@^3.1.5, istanbul-reports@^3.1.7: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterable-lookahead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/iterable-lookahead/-/iterable-lookahead-1.0.0.tgz#896dfcb78680bdb50036e97edb034c8b68a9737f" - integrity sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ== - iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -8417,7 +8362,7 @@ jest-worker@^29.6.3: merge-stream "^2.0.0" supports-color "^8.0.0" -jiti@^1.21.0: +jiti@^1.21.0, jiti@^1.21.6: version "1.21.6" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== @@ -8643,7 +8588,14 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.2, json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -8716,27 +8668,25 @@ klona@^2.0.4, klona@^2.0.6: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== -knip@^5.21.1: - version "5.22.0" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.22.0.tgz#e91620d833ed9331efc3e1dc079c5d8ab5c12f21" - integrity sha512-ijGbuB/622oL/rhg5kPR7U26xebu7czEu50RAxjh66YXkpvXW67Dv9Oz48yBSbGFMEWylX9aOe+NeHJbVe68jw== +knip@^5.30.1: + version "5.30.1" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.30.1.tgz#cfd4dca122b2bf1920fd99f2b49cc6ded60eff91" + integrity sha512-20XqtThAIuNNbEJjAdDTIUjSTx89bez5MukykKvaZEvLYLXOmtaXsSVPU6WQuFZ51/MolXctQllqHNzHxS210w== dependencies: - "@ericcornelissen/bash-parser" "0.5.3" - "@nodelib/fs.walk" "2.0.0" + "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" easy-table "1.2.0" + enhanced-resolve "^5.17.1" fast-glob "^3.3.2" - jiti "^1.21.0" + jiti "^1.21.6" js-yaml "^4.1.0" minimist "^1.2.8" picocolors "^1.0.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - resolve "^1.22.8" smol-toml "^1.1.4" strip-json-comments "5.0.1" summary "2.1.0" - tsconfig-paths "^4.2.0" zod "^3.22.4" zod-validation-error "^3.0.3" @@ -8850,7 +8800,7 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== -lilconfig@^3.0.0, lilconfig@~3.1.1: +lilconfig@^3.0.0, lilconfig@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== @@ -8860,32 +8810,32 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^15.2.7: - version "15.2.7" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" - integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== +lint-staged@^15.2.10: + version "15.2.10" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2" + integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg== dependencies: chalk "~5.3.0" commander "~12.1.0" - debug "~4.3.4" + debug "~4.3.6" execa "~8.0.1" - lilconfig "~3.1.1" - listr2 "~8.2.1" - micromatch "~4.0.7" + lilconfig "~3.1.2" + listr2 "~8.2.4" + micromatch "~4.0.8" pidtree "~0.6.0" string-argv "~0.3.2" - yaml "~2.4.2" + yaml "~2.5.0" -listr2@~8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.1.tgz#06a1a6efe85f23c5324180d7c1ddbd96b5eefd6d" - integrity sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g== +listr2@~8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" + integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20" eventemitter3 "^5.0.1" - log-update "^6.0.0" - rfdc "^1.3.1" + log-update "^6.1.0" + rfdc "^1.4.1" wrap-ansi "^9.0.0" load-json-file@^4.0.0: @@ -8976,11 +8926,6 @@ lodash.clonedeep@4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== -lodash.curry@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" - integrity sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA== - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -9059,14 +9004,14 @@ log-symbols@4.1.0, log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-update@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" - integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== dependencies: - ansi-escapes "^6.2.0" - cli-cursor "^4.0.0" - slice-ansi "^7.0.0" + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" strip-ansi "^7.1.0" wrap-ansi "^9.0.0" @@ -9174,13 +9119,6 @@ lz-utils@^2.0.2: resolved "https://registry.yarnpkg.com/lz-utils/-/lz-utils-2.0.2.tgz#9ccf1f76400617da5b3f5a05192f5227cffd6881" integrity sha512-i1PJN4hNEevkrvLMqNWCCac1BcB5SRaghywG7HVzWOyVkFOasLCG19ND1sY1F/ZEsM6SnGtoXyBWnmfqOM5r6g== -magic-string@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.16.0.tgz#970ebb0da7193301285fb1aa650f39bdd81eb45a" - integrity sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ== - dependencies: - vlq "^0.2.1" - magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -9234,11 +9172,6 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ== - map-obj@^4.0.0, map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" @@ -9531,10 +9464,10 @@ metro@0.80.9, metro@^0.80.3: ws "^7.5.1" yargs "^17.6.2" -micromatch@^4.0.4, micromatch@^4.0.5, micromatch@~4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== +micromatch@>=4.0.8, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@~4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" @@ -9586,6 +9519,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -9798,7 +9736,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -10108,16 +10046,6 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-pairs@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-pairs/-/object-pairs-0.1.0.tgz#8276eed81d60b8549d69c5f73a682ab9da4ff32f" - integrity sha512-3ECr6K831I4xX/Mduxr9UC+HPOz/d6WKKYj9p4cmC8Lg8p7g8gitzsxNX5IWlSIgFWN/a4JgrJaoAMKn20oKwA== - -object-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" - integrity sha512-+8hwcz/JnQ9EpLIXzN0Rs7DLsBpJNT/xYehtB/jU93tHYr5BFEO8E+JGQNOSqE7opVzz5cGksKFHt7uUJVLSjQ== - object.assign@^4.1.4, object.assign@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" @@ -10156,15 +10084,6 @@ object.groupby@^1.0.1: define-properties "^1.2.1" es-abstract "^1.23.2" -object.hasown@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" @@ -10231,6 +10150,13 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + open@^6.2.0: version "6.4.0" resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" @@ -10601,12 +10527,12 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== -payment@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/payment/-/payment-2.4.6.tgz#a69bfae5ee6edb2210d8c9f7720ee702359a40f5" - integrity sha512-QSCAa1yQSkqbe4Ghac3sSA5SQ+Cxc3e4xwCxxun5NT6hUSWsNXXlN8KCCY0kAFFXBP9C7DrfyXP4REB7nPJa8g== +payment@^2.4.7: + version "2.4.7" + resolved "https://registry.yarnpkg.com/payment/-/payment-2.4.7.tgz#fabefae499050f159e2a6837a58d30b78789f91c" + integrity sha512-5HyD3HJ0q3XtVm/Dss4t3KGtIughkCfnyQ1WfMashVD2vvBOmuXnkswhBcknMcHW8oo4zqVnNbQSkNaqafYaGA== dependencies: - globalthis "^1.0.2" + globalthis "^1.0.4" qj "~2.0.0" pend@~1.2.0: @@ -10626,7 +10552,12 @@ phin@^3.7.0: dependencies: centra "^2.7.0" -picocolors@^1.0.0, picocolors@^1.0.1: +picocolors@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + +picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== @@ -10716,17 +10647,17 @@ planby@^0.3.0: date-fns "^2.28.0" use-debounce "^7.0.1" -playwright-core@1.45.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.0.tgz#5741a670b7c9060ce06852c0051d84736fb94edc" - integrity sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ== +playwright-core@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.0.tgz#b54ec060fd83e5c2e46b63986b5ebb5e96ace427" + integrity sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg== -playwright@^1.44.5: - version "1.45.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.0.tgz#400c709c64438690f13705cb9c88ef93089c5c27" - integrity sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA== +playwright@^1.45.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.0.tgz#fb9b028883fad11362f9ff63ce7ba44bda0bf626" + integrity sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww== dependencies: - playwright-core "1.45.0" + playwright-core "1.47.0" optionalDependencies: fsevents "2.3.2" @@ -10905,9 +10836,9 @@ pretty-format@^29.7.0: react-is "^18.0.0" pretty-ms@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.0.0.tgz#53c57f81171c53be7ce3fd20bdd4265422bc5929" - integrity sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng== + version "9.1.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.1.0.tgz#0ad44de6086454f48a168e5abb3c26f8db1b3253" + integrity sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== dependencies: parse-ms "^4.0.0" @@ -11627,7 +11558,7 @@ resolve-options@^2.0.0: dependencies: value-or-function "^4.0.0" -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -11668,13 +11599,13 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -restore-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" + onetime "^7.0.0" + signal-exit "^4.1.0" retry@^0.10.0: version "0.10.1" @@ -11691,12 +11622,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -reverse-arguments@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reverse-arguments/-/reverse-arguments-1.0.0.tgz#c28095a3a921ac715d61834ddece9027992667cd" - integrity sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ== - -rfdc@^1.3.1: +rfdc@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== @@ -11796,7 +11722,7 @@ run-async@^2.2.0: resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -run-parallel@^1.1.9, run-parallel@^1.2.0: +run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== @@ -12107,11 +12033,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote-word@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/shell-quote-word/-/shell-quote-word-1.0.1.tgz#e2bdfd22d599fd68886491677e38f560f9d469c9" - integrity sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg== - shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" @@ -12199,7 +12120,7 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -slice-ansi@^7.0.0: +slice-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== @@ -12218,9 +12139,9 @@ smob@^1.0.0: integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== smol-toml@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.2.1.tgz#6216334548763d4aac76cafff19f8914937ee13a" - integrity sha512-OtZKrVrGIT+m++lxyF0z5n68nkwdgZotPhy89bfA4T7nSWe0xeQtfbjM1z5VLTilJdWXH46g8i0oAcpQNkzZTg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143" + integrity sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA== snake-case@^3.0.4: version "3.0.4" @@ -12457,7 +12378,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== @@ -12474,15 +12395,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" @@ -12493,19 +12405,14 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" string-width@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" - integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== dependencies: emoji-regex "^10.3.0" get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.fromcodepoint@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" - integrity sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg== - string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -12534,6 +12441,14 @@ string.prototype.padend@^3.0.0: es-abstract "^1.23.2" es-object-atoms "^1.0.0" +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -12585,7 +12500,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== @@ -12606,13 +12521,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.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" @@ -12868,6 +12776,11 @@ table@^6.8.1: string-width "^4.2.3" strip-ansi "^6.0.1" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -13103,18 +13016,6 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== -to-no-case@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" - integrity sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg== - -to-pascal-case@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-pascal-case/-/to-pascal-case-1.0.0.tgz#0bbdc8df448886ba01535e543327048d0aa1ce78" - integrity sha512-QGMWHqM6xPrcQW57S23c5/3BbYb0Tbe9p+ur98ckRnGDwD4wbbtDiYI38CfmMKNB5Iv0REjs5SNDntTwvDxzZA== - dependencies: - to-space-case "^1.0.0" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -13122,13 +13023,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-space-case@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" - integrity sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA== - dependencies: - to-no-case "^1.0.0" - to-through@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/to-through/-/to-through-3.0.0.tgz#bf4956eaca5a0476474850a53672bed6906ace54" @@ -13438,11 +13332,6 @@ ufo@^1.5.3: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -13483,13 +13372,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -unescape-js@^1.0.5: - version "1.1.4" - resolved "https://registry.yarnpkg.com/unescape-js/-/unescape-js-1.1.4.tgz#4bc6389c499cb055a98364a0b3094e1c3d5da395" - integrity sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g== - dependencies: - string.fromcodepoint "^0.2.1" - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -13591,6 +13473,11 @@ use-debounce@^7.0.1: resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-7.0.1.tgz#380e6191cc13ad29f8e2149a12b5c37cc2891190" integrity sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q== +use-sync-external-store@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + userhome@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/userhome/-/userhome-1.0.0.tgz#b6491ff12d21a5e72671df9ccc8717e1c6688c0b" @@ -13840,11 +13727,6 @@ vitest@^1.6.0: vite-node "1.6.0" why-is-node-running "^2.2.2" -vlq@^0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" - integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== - vlq@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" @@ -14061,10 +13943,10 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" -wicg-inert@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.2.tgz#df10cf756b773a96fce107c3ddcd43be5d1e3944" - integrity sha512-Ba9tGNYxXwaqKEi9sJJvPMKuo063umUPsHN0JJsjrs2j8KDSzkWLMZGZ+MH1Jf1Fq4OWZ5HsESJID6nRza2ang== +wicg-inert@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.3.tgz#e53dbc9ac1e0d7f8c60f25e707614a835986272a" + integrity sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ== word-wrap@^1.2.5: version "1.2.5" @@ -14392,7 +14274,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": +"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== @@ -14410,15 +14292,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.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== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -14469,36 +14342,10 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -ws@8.17.1, ws@^8.13.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -ws@^3.2.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - -ws@^6.2.2: - version "6.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" - integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== - dependencies: - async-limiter "~1.0.0" - -ws@^7, ws@^7.0.0, ws@^7.5.0, ws@^7.5.1: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@8.13.0, ws@8.17.1, ws@>=5.2.4, ws@^3.2.0, ws@^6.2.2, ws@^7, ws@^7.0.0, ws@^7.5.0, ws@^7.5.1, ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xdg-basedir@^4.0.0: version "4.0.0" @@ -14560,11 +14407,16 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.1, yaml@~2.4.2: +yaml@^2.2.1: version "2.4.5" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== +yaml@~2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" + integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" @@ -14691,16 +14543,18 @@ yup@^0.32.11: toposort "^2.0.2" zod-validation-error@^3.0.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.0.tgz#2cfe81b62d044e0453d1aa3ae7c32a2f36dde9af" - integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw== + version "3.3.1" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.1.tgz#86adc781129d1a7fed3c3e567e8dbe7c4a15eaa4" + integrity sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA== zod@3.23.8, zod@^3.22.4: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== -zustand@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" - integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== +zustand@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" + integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + dependencies: + use-sync-external-store "1.2.2" From 80acd7f6c6672baf6e4a01dbecdf8540be68dbd7 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:46:45 +0300 Subject: [PATCH 04/12] feat(menu): support media type for menu (#610) * feat(menu): support media type for menu * feat(menu): fix lint fail * feat(menu): code cleanup, rename types to include media * feat(menu): add use case for playlist explicitly * feat(menu): return empty string for default case * feat(menu): rename constant --- packages/common/src/constants.ts | 3 ++- packages/common/src/utils/urlFormatting.ts | 20 +++++++++++++++++++ packages/common/types/config.ts | 14 +++++++------ packages/hooks-react/src/usePlaylist.ts | 12 +++++------ packages/hooks-react/src/usePlaylists.ts | 4 ++-- .../ui-react/src/components/Shelf/Shelf.tsx | 4 ++-- .../ui-react/src/containers/Layout/Layout.tsx | 5 ++--- .../SidebarContainer/SidebarContainer.tsx | 6 +++--- .../ScreenRouting/PlaylistScreenRouter.tsx | 8 ++++---- .../src/containers/AppRoutes/AppRoutes.tsx | 6 +++--- 10 files changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index b63801922..6ad7b7a29 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -86,9 +86,10 @@ export const EPG_TYPE = { viewNexa: 'viewnexa', } as const; -export const PLAYLIST_TYPE = { +export const APP_CONFIG_ITEM_TYPE = { playlist: 'playlist', continue_watching: 'continue_watching', favorites: 'favorites', content_list: 'content_list', + media: 'media', } as const; diff --git a/packages/common/src/utils/urlFormatting.ts b/packages/common/src/utils/urlFormatting.ts index 09994e790..cc968c82f 100644 --- a/packages/common/src/utils/urlFormatting.ts +++ b/packages/common/src/utils/urlFormatting.ts @@ -1,6 +1,9 @@ +import type { AppMenuType } from '@jwp/ott-common/types/config'; + import type { PlaylistItem } from '../../types/playlist'; import { PATH_MEDIA, PATH_PLAYLIST, PATH_CONTENT_LIST } from '../paths'; import { logWarn } from '../logger'; +import { APP_CONFIG_ITEM_TYPE } from '../constants'; import { getLegacySeriesPlaylistIdFromEpisodeTags, getSeriesPlaylistIdFromCustomParams } from './media'; @@ -117,6 +120,10 @@ export const mediaURL = ({ ); }; +export const singleMediaURL = (id: string, title?: string) => { + return createPath(PATH_MEDIA, { id, title: title ? slugify(title) : undefined }); +}; + export const playlistURL = (id: string, title?: string) => { return createPath(PATH_PLAYLIST, { id, title: title ? slugify(title) : undefined }); }; @@ -125,6 +132,19 @@ export const contentListURL = (id: string, title?: string) => { return createPath(PATH_CONTENT_LIST, { id, title: title ? slugify(title) : undefined }); }; +export const determinePath = ({ type, contentId }: { type: AppMenuType | undefined; contentId: string }) => { + switch (type) { + case APP_CONFIG_ITEM_TYPE.content_list: + return contentListURL(contentId); + case APP_CONFIG_ITEM_TYPE.media: + return singleMediaURL(contentId); + case APP_CONFIG_ITEM_TYPE.playlist: + return playlistURL(contentId); + default: + return ''; + } +}; + export const liveChannelsURL = (playlistId: string, channelId?: string, play = false) => { return createPath( PATH_PLAYLIST, diff --git a/packages/common/types/config.ts b/packages/common/types/config.ts index d3b62cfe9..2d8c980ee 100644 --- a/packages/common/types/config.ts +++ b/packages/common/types/config.ts @@ -1,4 +1,4 @@ -import type { PLAYLIST_TYPE } from '../src/constants'; +import type { APP_CONFIG_ITEM_TYPE } from '../src/constants'; import type { AdScheduleUrls, AdDeliveryMethod } from './ad-schedule'; @@ -43,14 +43,14 @@ export type Drm = { defaultPolicyId: string; }; -export type PlaylistType = keyof typeof PLAYLIST_TYPE; - -export type PlaylistMenuType = Extract; +export type AppContentType = keyof typeof APP_CONFIG_ITEM_TYPE; +export type AppMenuType = Extract; +export type AppShelfType = Extract; export type Content = { contentId?: string; title?: string; - type: PlaylistType; + type: AppShelfType; featured?: boolean; backgroundColor?: string | null; }; @@ -58,7 +58,7 @@ export type Content = { export type Menu = { label: string; contentId: string; - type?: PlaylistMenuType; + type?: AppMenuType; filterTags?: string; }; @@ -78,11 +78,13 @@ export type Cleeng = { yearlyOffer?: string | null; useSandbox?: boolean; }; + export type JWP = { clientId?: string | null; assetId?: number | null; useSandbox?: boolean; }; + export type Features = { recommendationsPlaylist?: string | null; searchPlaylist?: string | null; diff --git a/packages/hooks-react/src/usePlaylist.ts b/packages/hooks-react/src/usePlaylist.ts index 9637731c1..5e8ed77b5 100644 --- a/packages/hooks-react/src/usePlaylist.ts +++ b/packages/hooks-react/src/usePlaylist.ts @@ -6,8 +6,8 @@ import { generatePlaylistPlaceholder } from '@jwp/ott-common/src/utils/collectio import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; import type { ApiError } from '@jwp/ott-common/src/utils/api'; -import type { PlaylistMenuType } from '@jwp/ott-common/types/config'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import type { AppMenuType } from '@jwp/ott-common/types/config'; +import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; const placeholderData = generatePlaylistPlaceholder(30); @@ -21,7 +21,7 @@ export const getPlaylistQueryOptions = ({ params = {}, queryClient, }: { - type: PlaylistMenuType; + type: AppMenuType; contentId: string | undefined; siteId: string; enabled: boolean; @@ -35,7 +35,7 @@ export const getPlaylistQueryOptions = ({ enabled: !!contentId && enabled, queryKey: ['playlist', type, contentId, params], queryFn: async () => { - if (type === PLAYLIST_TYPE.playlist) { + if (type === APP_CONFIG_ITEM_TYPE.playlist) { const playlist = await apiService.getPlaylistById(contentId, params); // This pre-caches all playlist items and makes navigating a lot faster. @@ -44,7 +44,7 @@ export const getPlaylistQueryOptions = ({ }); return playlist; - } else if (type === PLAYLIST_TYPE.content_list) { + } else if (type === APP_CONFIG_ITEM_TYPE.content_list) { const contentList = await apiService.getContentList({ siteId, id: contentId }); return contentList; @@ -67,7 +67,7 @@ export default function usePlaylist( params: GetPlaylistParams = {}, enabled: boolean = true, usePlaceholderData: boolean = true, - type: PlaylistMenuType = PLAYLIST_TYPE.playlist, + type: AppMenuType = APP_CONFIG_ITEM_TYPE.playlist, ) { const queryClient = useQueryClient(); const siteId = useConfigStore((state) => state.config.siteId); diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts index 3b0d6c7f5..14e658246 100644 --- a/packages/hooks-react/src/usePlaylists.ts +++ b/packages/hooks-react/src/usePlaylists.ts @@ -2,7 +2,7 @@ import { PersonalShelf, PersonalShelves, PLAYLIST_LIMIT } from '@jwp/ott-common/ import { useFavoritesStore } from '@jwp/ott-common/src/stores/FavoritesStore'; import { useWatchHistoryStore } from '@jwp/ott-common/src/stores/WatchHistoryStore'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; -import type { Content, PlaylistMenuType, PlaylistType } from '@jwp/ott-common/types/config'; +import type { Content, AppContentType, AppMenuType } from '@jwp/ott-common/types/config'; import type { Playlist } from '@jwp/ott-common/types/playlist'; import { useQueries, useQueryClient } from 'react-query'; @@ -15,7 +15,7 @@ type UsePlaylistResult = { isPlaceholderData?: boolean; }[]; -const isPlaylistType = (type: PlaylistType): type is PlaylistMenuType => !PersonalShelves.some((pType) => pType === type); +const isPlaylistType = (type: AppContentType): type is AppMenuType => !PersonalShelves.some((pType) => pType === type); const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undefined) => { const page_limit = PLAYLIST_LIMIT.toString(); diff --git a/packages/ui-react/src/components/Shelf/Shelf.tsx b/packages/ui-react/src/components/Shelf/Shelf.tsx index cec054ba7..92c9eb2d6 100644 --- a/packages/ui-react/src/components/Shelf/Shelf.tsx +++ b/packages/ui-react/src/components/Shelf/Shelf.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { CYCLE_MODE_RESTART, type RenderControl, type RenderPagination, TileSlider } from '@videodock/tile-slider'; import type { Playlist, PlaylistItem } from '@jwp/ott-common/types/playlist'; -import type { AccessModel, PlaylistType } from '@jwp/ott-common/types/config'; +import type { AccessModel, AppContentType } from '@jwp/ott-common/types/config'; import { isLocked } from '@jwp/ott-common/src/utils/entitlements'; import { mediaURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { PersonalShelf } from '@jwp/ott-common/src/constants'; @@ -39,7 +39,7 @@ export const ShelfIdentifier = Symbol(`SHELF`); export type ShelfProps = { playlist: Playlist; - type: PlaylistType; + type: AppContentType; onCardHover?: (playlistItem: PlaylistItem) => void; watchHistory?: { [key: string]: number }; enableTitle?: boolean; diff --git a/packages/ui-react/src/containers/Layout/Layout.tsx b/packages/ui-react/src/containers/Layout/Layout.tsx index 5296ed960..ce727d2fb 100644 --- a/packages/ui-react/src/containers/Layout/Layout.tsx +++ b/packages/ui-react/src/containers/Layout/Layout.tsx @@ -4,10 +4,9 @@ import { Outlet } from 'react-router'; import { shallow } from '@jwp/ott-common/src/utils/compare'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { unicodeToChar } from '@jwp/ott-common/src/utils/common'; -import { contentListURL, playlistURL } from '@jwp/ott-common/src/utils/urlFormatting'; +import { determinePath } from '@jwp/ott-common/src/utils/urlFormatting'; import env from '@jwp/ott-common/src/env'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; import Header from '../../components/Header/Header'; import Footer from '../../components/Footer/Footer'; @@ -51,7 +50,7 @@ const Layout = () => { { label: t('home'), to: '/' }, ...menu.map(({ label, contentId, type }) => ({ label, - to: type === PLAYLIST_TYPE.content_list ? contentListURL(contentId) : playlistURL(contentId), + to: determinePath({ type, contentId }), })), ]; diff --git a/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx b/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx index 527c412ff..47b31029a 100644 --- a/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx +++ b/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { contentListURL, playlistURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import useOpaqueId from '@jwp/ott-hooks-react/src/useOpaqueId'; import { useLocation, useNavigate } from 'react-router'; -import { ACCESS_MODEL, PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; +import { determinePath } from '@jwp/ott-common/src/utils/urlFormatting'; import Button from '../../components/Button/Button'; import Sidebar from '../../components/Sidebar/Sidebar'; @@ -84,7 +84,7 @@ const SidebarContainer = () => { {menu.map(({ contentId, type, label }) => (
  • - +
  • ))} diff --git a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx index 6f12fbe70..1663b49a3 100644 --- a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import type { Playlist } from '@jwp/ott-common/types/playlist'; -import { PLAYLIST_TYPE, PLAYLIST_CONTENT_TYPE } from '@jwp/ott-common/src/constants'; +import { APP_CONFIG_ITEM_TYPE, PLAYLIST_CONTENT_TYPE } from '@jwp/ott-common/src/constants'; import { ScreenMap } from '@jwp/ott-common/src/utils/ScreenMap'; import usePlaylist from '@jwp/ott-hooks-react/src/usePlaylist'; -import type { PlaylistMenuType } from '@jwp/ott-common/types/config'; +import type { AppMenuType } from '@jwp/ott-common/types/config'; import Loading from '../Loading/Loading'; import ErrorPage from '../../components/ErrorPage/ErrorPage'; @@ -24,7 +24,7 @@ playlistScreenMap.registerByContentType(PlaylistLiveChannels, PLAYLIST_CONTENT_T // register content list screens contentScreenMap.registerDefault(PlaylistGrid); -const PlaylistScreenRouter = ({ type }: { type: PlaylistMenuType }) => { +const PlaylistScreenRouter = ({ type }: { type: AppMenuType }) => { const params = useParams(); const id = params.id || ''; @@ -43,7 +43,7 @@ const PlaylistScreenRouter = ({ type }: { type: PlaylistMenuType }) => { return ; } - const Screen = type === PLAYLIST_TYPE.content_list ? contentScreenMap.getScreen(data) : playlistScreenMap.getScreen(data); + const Screen = type === APP_CONFIG_ITEM_TYPE.content_list ? contentScreenMap.getScreen(data) : playlistScreenMap.getScreen(data); return ; }; diff --git a/platforms/web/src/containers/AppRoutes/AppRoutes.tsx b/platforms/web/src/containers/AppRoutes/AppRoutes.tsx index 4d71c4739..74a964a3d 100644 --- a/platforms/web/src/containers/AppRoutes/AppRoutes.tsx +++ b/platforms/web/src/containers/AppRoutes/AppRoutes.tsx @@ -12,7 +12,7 @@ import MediaScreenRouter from '@jwp/ott-ui-react/src/pages/ScreenRouting/MediaSc import PlaylistScreenRouter from '@jwp/ott-ui-react/src/pages/ScreenRouting/PlaylistScreenRouter'; import Layout from '@jwp/ott-ui-react/src/containers/Layout/Layout'; import { PATH_ABOUT, PATH_CONTENT_LIST, PATH_LEGACY_SERIES, PATH_MEDIA, PATH_PLAYLIST, PATH_SEARCH, PATH_USER } from '@jwp/ott-common/src/paths'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import RoutesContainer from '#src/containers/RoutesContainer/RoutesContainer'; @@ -24,8 +24,8 @@ export default function AppRoutes() { }> } errorElement={}> } /> - } /> - } /> + } /> + } /> } /> } /> } /> From b081d512de3154b13afd568333e717c39b1a9478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Thu, 19 Sep 2024 12:51:09 +0200 Subject: [PATCH 05/12] feat(project): add new way of handling media with passport --- .../src/controllers/AccessController.ts | 34 ++++++++++++++- .../src/controllers/AccountController.ts | 1 + packages/common/src/env.ts | 2 +- packages/common/src/services/ApiService.ts | 19 ++++++++ .../src/services/JWPEntitlementService.ts | 2 +- .../integrations/jwp/JWPAPIService.ts | 2 +- packages/common/src/stores/AccessStore.ts | 13 ++++++ packages/common/src/utils/sources.ts | 43 ++++++++++++++++++- packages/common/types/plans.ts | 10 +++-- packages/common/types/playlist.ts | 13 ++++++ .../hooks-react/src/useContentProtection.ts | 13 +++++- packages/hooks-react/src/useMediaSources.ts | 4 +- packages/hooks-react/src/useProtectedMedia.ts | 39 ++++++++++++++++- platforms/web/.env | 2 +- platforms/web/ini/.webapp.dev.ini | 4 +- 15 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 packages/common/src/stores/AccessStore.ts diff --git a/packages/common/src/controllers/AccessController.ts b/packages/common/src/controllers/AccessController.ts index 99cd7a70e..67d9ea179 100644 --- a/packages/common/src/controllers/AccessController.ts +++ b/packages/common/src/controllers/AccessController.ts @@ -8,11 +8,14 @@ import { INTEGRATION_TYPE } from '../modules/types'; import { getNamedModule } from '../modules/container'; import StorageService from '../services/StorageService'; import type { AccessTokens } from '../../types/access'; +import { useAccessStore } from '../stores/AccessStore'; +import JWPEntitlementService from '../services/JWPEntitlementService'; const ACCESS_TOKENS = 'access_tokens'; @injectable() export default class AccessController { + private readonly entitlementService: JWPEntitlementService; private readonly accessService: AccessService; private readonly accountService: AccountService; private readonly storageService: StorageService; @@ -21,9 +24,11 @@ export default class AccessController { constructor( @inject(INTEGRATION_TYPE) integrationType: IntegrationType, + @inject(JWPEntitlementService) entitlementService: JWPEntitlementService, @inject(StorageService) storageService: StorageService, @inject(AccessService) accessService: AccessService, ) { + this.entitlementService = entitlementService; this.accessService = accessService; this.storageService = storageService; this.accountService = getNamedModule(AccountService, integrationType); @@ -41,6 +46,25 @@ export default class AccessController { // Not awaiting to avoid blocking the loading process, // as the access tokens can be stored asynchronously without affecting the app's performance void this.generateOrRefreshAccessTokens(); + + // Fetches the entitled plans for the viewer and stores them into the access store. + await this.fetchAndStoreEntitledPlans(); + }; + + getMediaById = async () => {}; + + fetchAndStoreEntitledPlans = async () => { + if (!this.siteId) { + return; + } + // Note: Without a valid plan ID, requests for media metadata cannot be made. + // TODO: Support for multiple plans should be added. Revisit this logic once the dependency on plan_id is changed. + const response = await this.entitlementService.getEntitledPlans({ siteId: this.siteId }); + if (response?.plans?.length) { + // Find the SVOD plan or fallback to the first available plan + const entitledPlan = response.plans.find((plan) => plan.metadata.access_model === 'svod') || response.plans[0]; + useAccessStore.setState({ entitledPlan }); + } }; generateOrRefreshAccessTokens = async () => { @@ -85,6 +109,8 @@ export default class AccessController { }; setAccessTokens = async (accessTokens: AccessTokens) => { + useAccessStore.setState({ passport: accessTokens.passport }); + // Since the actual valid time for a passport token is 1 hour, set the expires to one hour from now. // The expires field here is used as a helper to manage the passport's validity and refresh process. const expires = new Date(Date.now() + 3600 * 1000).getTime(); @@ -92,10 +118,16 @@ export default class AccessController { }; getAccessTokens = async (): Promise<(AccessTokens & { expires: number }) | null> => { - return await this.storageService.getItem(ACCESS_TOKENS, true, true); + const accessTokens = await this.storageService.getItem(ACCESS_TOKENS, true, true); + if (accessTokens) { + useAccessStore.setState({ passport: accessTokens.passport }); + } + + return accessTokens; }; removeAccessTokens = async () => { + useAccessStore.setState({ passport: null }); return await this.storageService.removeItem(ACCESS_TOKENS); }; } diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index 0c096576b..f54761476 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -168,6 +168,7 @@ export default class AccountController { if (response) { void this.accessController?.generateAccessTokens(); + void this.accessController?.fetchAndStoreEntitledPlans(); await this.afterLogin(response.user, response.customerConsents); return; } diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index be18d8ac1..a791fea8e 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://cdn-dev.jwplayer.com', + APP_API_BASE_URL: 'https://cdn.jwplayer.com', APP_API_ACCESS_BRIDGE_URL: '', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index 6cf199d1d..fd16a6161 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -169,6 +169,25 @@ export default class ApiService { return this.transformMediaItem(mediaItem); }; + /** + * Get media by id + * @param {string} mediaId + * @param {string} siteId + * @param {string} planId + * @param {string} passport + */ + getMediaByIdWithPassport = async (mediaId: string, siteId: string, planId: string, passport: string): Promise => { + const pathname = `/v2/sites/${siteId}/media/${mediaId}/playback.json`; + const url = createURL(`${env.APP_API_BASE_URL}${pathname}`, { passport, plan_id: planId }); + const response = await fetch(url); + const data = (await getDataOrThrow(response)) as Playlist; + const mediaItem = data.playlist[0]; + + if (!mediaItem) throw new Error('MediaItem not found'); + + return this.transformMediaItem(mediaItem); + }; + /** * Get series by id * @param {string} id diff --git a/packages/common/src/services/JWPEntitlementService.ts b/packages/common/src/services/JWPEntitlementService.ts index 08d575279..04003abbe 100644 --- a/packages/common/src/services/JWPEntitlementService.ts +++ b/packages/common/src/services/JWPEntitlementService.ts @@ -40,7 +40,7 @@ export default class JWPEntitlementService { }); return data; } catch { - throw new Error('Failed to get entitled plans'); + throw new Error('Failed to fetch entitled plans'); } }; } diff --git a/packages/common/src/services/integrations/jwp/JWPAPIService.ts b/packages/common/src/services/integrations/jwp/JWPAPIService.ts index 47c4ddd60..86439a7e0 100644 --- a/packages/common/src/services/integrations/jwp/JWPAPIService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAPIService.ts @@ -34,7 +34,7 @@ export default class JWPAPIService { this.useSandboxEnv = useSandboxEnv; }; - private getBaseUrl = () => (this.useSandboxEnv ? 'https://daily-sims.jwplayer.com' : 'https://sims.jwplayer.com'); + private getBaseUrl = () => (this.useSandboxEnv ? 'https://staging-sims.jwplayer.com' : 'https://sims.jwplayer.com'); setToken = (token: string, refreshToken = '', expires: number) => { return this.storageService.setItem(INPLAYER_TOKEN_KEY, JSON.stringify({ token, refreshToken, expires }), false); diff --git a/packages/common/src/stores/AccessStore.ts b/packages/common/src/stores/AccessStore.ts new file mode 100644 index 000000000..493c7cbe3 --- /dev/null +++ b/packages/common/src/stores/AccessStore.ts @@ -0,0 +1,13 @@ +import type { Plan } from '../../types/plans'; + +import { createStore } from './utils'; + +type AccessStore = { + passport: string | null; + entitledPlan: Plan | null; +}; + +export const useAccessStore = createStore('AccessStore', () => ({ + passport: null, + entitledPlan: null, +})); diff --git a/packages/common/src/utils/sources.ts b/packages/common/src/utils/sources.ts index c96a35022..6ed2f69a2 100644 --- a/packages/common/src/utils/sources.ts +++ b/packages/common/src/utils/sources.ts @@ -10,7 +10,19 @@ const isBCLManifestType = (sourceUrl: string, baseUrl: string, mediaId: string, return extensions.some((ext) => sourceUrl === `${baseUrl}/live/broadcast/${mediaId}.${ext}`); }; -export const getSources = ({ item, baseUrl, config, user }: { item: PlaylistItem; baseUrl: string; config: Config; user: Customer | null }) => { +export const getSources = ({ + item, + baseUrl, + config, + user, + passport, +}: { + item: PlaylistItem; + baseUrl: string; + config: Config; + user: Customer | null; + passport: string | null; +}) => { const { sources, mediaid } = item; const { adConfig, siteId, adDeliveryMethod } = config; @@ -34,10 +46,37 @@ export const getSources = ({ item, baseUrl, config, user }: { item: PlaylistItem // Attach user_id only for VOD and BCL SaaS Live Streams (doesn't work with SSAI items) } else if ((isVODManifest || isBCLManifest) && userId) { url.searchParams.set('user_id', userId); + } else if (passport) { + // Attach the passport in all the drm sources as it's needed for the licence request. + // Passport is only available if Access Bridge is in use. + attachPassportToSourceWithDRM(source, passport); } source.file = url.toString(); - return source; }); }; + +function attachPassportToSourceWithDRM(source: Source, passport: string): Source { + function updateUrl(urlString: string, passport: string): string { + const url = new URL(urlString); + if (!url.searchParams.has('token')) { + url.searchParams.set('passport', passport); + } + return url.toString(); + } + + if (source?.drm) { + if (source.drm?.playready?.url) { + source.drm.playready.url = updateUrl(source.drm.playready.url, passport); + } + if (source.drm?.widevine?.url) { + source.drm.widevine.url = updateUrl(source.drm.widevine.url, passport); + } + if (source.drm?.fairplay?.processSpcUrl) { + source.drm.fairplay.processSpcUrl = updateUrl(source.drm.fairplay.processSpcUrl, passport); + } + } + + return source; +} diff --git a/packages/common/types/plans.ts b/packages/common/types/plans.ts index fff88440c..400703850 100644 --- a/packages/common/types/plans.ts +++ b/packages/common/types/plans.ts @@ -18,11 +18,13 @@ export type AccessControlPlan = { }; export type Plan = { - name: string; - access_model: 'free' | 'freeauth' | 'svod'; - access_plan: AccessControlPlan; - access: AccessOptions; + id: string; + original_id: number; + exp: number; metadata: { + name: string; + access: AccessOptions; + access_model: 'free' | 'freeauth' | 'svod'; external_providers: PlanExternalProviders; }; }; diff --git a/packages/common/types/playlist.ts b/packages/common/types/playlist.ts index 26c5731c3..5d08d9ca2 100644 --- a/packages/common/types/playlist.ts +++ b/packages/common/types/playlist.ts @@ -8,9 +8,22 @@ export type Image = { width: number; }; +export type DRM = { + playready?: { + url: string; + }; + widevine?: { + url: string; + }; + fairplay?: { + processSpcUrl: string; + }; +}; + export type Source = { file: string; type: string; + drm?: DRM; }; export type Track = { diff --git a/packages/hooks-react/src/useContentProtection.ts b/packages/hooks-react/src/useContentProtection.ts index a3598af93..f7dfffae8 100644 --- a/packages/hooks-react/src/useContentProtection.ts +++ b/packages/hooks-react/src/useContentProtection.ts @@ -20,13 +20,15 @@ const useContentProtection = ( const genericEntitlementService = getModule(GenericEntitlementService); const jwpEntitlementService = getModule(JWPEntitlementService); - const { configId, signingConfig, contentProtection, jwp, urlSigning } = useConfigStore(({ config }) => ({ + const { configId, signingConfig, contentProtection, jwp, urlSigning, isAcessBridgeEnabled } = useConfigStore(({ config, settings }) => ({ configId: config.id, signingConfig: config.contentSigningService, contentProtection: config.contentProtection, jwp: config.integrations.jwp, urlSigning: isTruthyCustomParamValue(config?.custom?.urlSigning), + isAcessBridgeEnabled: !!settings?.apiAccessBridgeUrl, })); + const host = signingConfig?.host; const drmPolicyId = contentProtection?.drm?.defaultPolicyId ?? signingConfig?.drmPolicyId; const signingEnabled = !!urlSigning || !!host || (!!drmPolicyId && !host); @@ -34,6 +36,12 @@ const useContentProtection = ( const { data: token, isLoading } = useQuery( ['token', type, id, params], async () => { + if (isAcessBridgeEnabled) { + // if access control is set up we return nothing + // the useProtectedMedia hook will take care of it + return; + } + // if provider is not JWP if (!!id && !!host) { const accountController = getModule(AccountController); @@ -42,12 +50,13 @@ const useContentProtection = ( 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: (signingEnabled || isAcessBridgeEnabled) && enabled && !!id, keepPreviousData: false, staleTime: 15 * 60 * 1000 }, ); const queryResult = useQuery([type, id, params, token], async () => callback(token, drmPolicyId), { diff --git a/packages/hooks-react/src/useMediaSources.ts b/packages/hooks-react/src/useMediaSources.ts index 8f344f018..beca034f0 100644 --- a/packages/hooks-react/src/useMediaSources.ts +++ b/packages/hooks-react/src/useMediaSources.ts @@ -3,11 +3,13 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; import type { PlaylistItem, Source } from '@jwp/ott-common/types/playlist'; import { getSources } from '@jwp/ott-common/src/utils/sources'; +import { useAccessStore } from '@jwp/ott-common/src/stores/AccessStore'; /** Modify manifest URLs to handle server ads and analytics params */ export const useMediaSources = ({ item, baseUrl }: { item: PlaylistItem; baseUrl: string }): Source[] => { const config = useConfigStore((s) => s.config); const user = useAccountStore((s) => s.user); + const passport = useAccessStore((s) => s.passport); - return useMemo(() => getSources({ item, baseUrl, config, user }), [item, baseUrl, config, user]); + return useMemo(() => getSources({ item, baseUrl, config, user, passport }), [item, baseUrl, config, user, passport]); }; diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index eeb0e4a4a..e073ef004 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -2,12 +2,49 @@ import { useQuery } from 'react-query'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; +import { useAccessStore } from '@jwp/ott-common/src/stores/AccessStore'; +import AccessController from '@jwp/ott-common/src/controllers/AccessController'; +import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { ApiError } from '@jwp/ott-common/src/utils/api'; import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); - const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => apiService.getMediaById(item.mediaid, token, drmPolicyId)); + const accessController = getModule(AccessController); + + const { siteId } = useConfigStore().config; + const { passport, entitledPlan } = useAccessStore(); + + const getMedia = async (token?: string, drmPolicyId?: string) => { + // If nothing from Access Bridge is present, the flow remains as it was. + if (!passport || !entitledPlan) { + return apiService.getMediaById(item.mediaid, token, drmPolicyId); + } + + // Otherwise use passport to get the media + // TODO: the logic needs to be revisited once the dependency on planId is changed. + try { + return await apiService.getMediaByIdWithPassport(item.mediaid, siteId, entitledPlan.id, passport); + } catch (error: unknown) { + if (error instanceof ApiError && error.code === 403) { + // If the passport is invalid or expired, refresh and get media again + await accessController.refreshAccessTokens(); + const updatedPassport = useAccessStore.getState().passport; + + if (updatedPassport) { + return await apiService.getMediaByIdWithPassport(item.mediaid, siteId, entitledPlan.id, updatedPassport); + } + + throw new Error('Failed to refresh passport and retrieve media.'); + } + + throw error; + } + }; + + const contentProtectionQuery = useContentProtection('media', item.mediaid, async (token, drmPolicyId) => getMedia(token, drmPolicyId)); + const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], () => { diff --git a/platforms/web/.env b/platforms/web/.env index e03c4ce15..746f0aa36 100644 --- a/platforms/web/.env +++ b/platforms/web/.env @@ -1,4 +1,4 @@ -APP_API_BASE_URL=https://cdn-dev.jwplayer.com +APP_API_BASE_URL=https://cdn.jwplayer.com APP_PLAYER_ID=M4qoGvUk # page metadata (SEO) diff --git a/platforms/web/ini/.webapp.dev.ini b/platforms/web/ini/.webapp.dev.ini index a3b32dfc2..e4d380896 100644 --- a/platforms/web/ini/.webapp.dev.ini +++ b/platforms/web/ini/.webapp.dev.ini @@ -3,4 +3,6 @@ defaultConfigSource = gnnuzabk ; When developing, switching between configs is useful for test and debug UNSAFE_allowAnyConfigSource = true ; Access Bridge service API url host -apiAccessBridgeUrl= +apiAccessBridgeUrl = + + From e538e402966d966420fefbf10a54f6c00989bf9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Thu, 19 Sep 2024 12:56:58 +0200 Subject: [PATCH 06/12] chore: nit in webapp.dev.ini --- platforms/web/ini/.webapp.dev.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/platforms/web/ini/.webapp.dev.ini b/platforms/web/ini/.webapp.dev.ini index e4d380896..850dc82fd 100644 --- a/platforms/web/ini/.webapp.dev.ini +++ b/platforms/web/ini/.webapp.dev.ini @@ -4,5 +4,3 @@ defaultConfigSource = gnnuzabk UNSAFE_allowAnyConfigSource = true ; Access Bridge service API url host apiAccessBridgeUrl = - - From 557f2e58ee6c82f4b8d4adccd5532d70d361982f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Thu, 19 Sep 2024 13:13:04 +0200 Subject: [PATCH 07/12] fix: add Mock for Access controller in cinema test --- packages/ui-react/src/containers/Cinema/Cinema.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui-react/src/containers/Cinema/Cinema.test.tsx b/packages/ui-react/src/containers/Cinema/Cinema.test.tsx index 1711f49e4..d93b72fea 100644 --- a/packages/ui-react/src/containers/Cinema/Cinema.test.tsx +++ b/packages/ui-react/src/containers/Cinema/Cinema.test.tsx @@ -3,6 +3,7 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import { beforeEach } from 'vitest'; import { mockService } from '@jwp/ott-common/test/mockService'; import ApiService from '@jwp/ott-common/src/services/ApiService'; +import AccessController from '@jwp/ott-common/src/controllers/AccessController'; import GenericEntitlementService from '@jwp/ott-common/src/services/GenericEntitlementService'; import JWPEntitlementService from '@jwp/ott-common/src/services/JWPEntitlementService'; import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; @@ -18,6 +19,7 @@ describe('', () => { mockService(GenericEntitlementService, {}); mockService(JWPEntitlementService, {}); mockService(WatchHistoryController, {}); + mockService(AccessController, {}); }); test('renders and matches snapshot', async () => { From e116b1274f63d893b920957d26172248c097cd91 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:39:46 +0300 Subject: [PATCH 08/12] [OWA-79] feat(i18n): add translations (#602) * feat(i18n): add custom params translations for media title and description * feat(i18n): add custom params translations for all translatable fields * feat(i18n): update getTranslatableFields function * feat(i18n): clean up getTranslatedFields function * feat(i18n): make language optional, clean up code * feat(i18n): remove hardcoded default language en and replace with env variable * feat(i18n): pass language to usePlaylist * feat(i18n): add language for useSearch * feat(i18n): add language to query key * feat(i18n): remove custom built hook, use i18n.language instead * feat(i18n): get language directly inside the hooks * feat(i18n): revert usePlaylist function signature * feat(i18n): revert usePlaylist to initial code usage * feat(i18n): revert function signature for useMedia * feat(i18n): fix order for code * feat(i18n): remove language import for useProtectedMedia * feat(i18n): use local storage for favorites, watchlist in combination with page reload * feat(i18n): use i18next instead of local storage * feat(i18n): remove reload code from language switcher * feat(i18n): fix language switch for favorites and continue watching * feat(i18n): pass language for app init as well * feat(i18n): clean up code * feat(i18n): fix test fail * feat(i18n): rename variable to better reflect function utility * feat(i18n): use i18n directly * feat(i18n): clean up functions * feat(i18n): fix getMediaById * feat(i18n): code cleanup * feat(i18n): clean up functions signatures * feat(i18n): get language directly in watchlist initialize function * feat(i18n): revert change for initI18n * feat(i18n): revert small formatting changes, irrelevant for this task * feat(i18n): revert to using useTranslation hook * feat(i18n): remove unnecessary type cast * feat(i18n): remove menu from useTranslation --- .../common/src/controllers/AppController.ts | 6 +- .../src/controllers/FavoritesController.ts | 8 +- .../src/controllers/WatchHistoryController.ts | 8 +- packages/common/src/services/ApiService.ts | 82 ++++++++++++++----- .../common/src/services/FavoriteService.ts | 4 +- .../src/services/WatchHistoryService.ts | 19 +++-- .../hooks-react/src/series/useEpisodes.ts | 11 ++- .../hooks-react/src/series/useNextEpisode.ts | 9 +- packages/hooks-react/src/useBootstrapApp.ts | 5 +- packages/hooks-react/src/useMedia.ts | 7 +- packages/hooks-react/src/usePlaylist.ts | 15 +++- packages/hooks-react/src/usePlaylists.ts | 5 ++ packages/hooks-react/src/useProtectedMedia.ts | 4 +- .../HeaderLanguageSwitcher.tsx | 8 ++ .../src/containers/Layout/Layout.test.tsx | 4 + 15 files changed, 142 insertions(+), 53 deletions(-) diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index 6d86d6d36..474d04c86 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -59,7 +59,7 @@ export default class AppController { return config; }; - initializeApp = async (url: string, refreshEntitlements?: () => Promise) => { + initializeApp = async (url: string, language: string, refreshEntitlements?: () => Promise) => { logDebug('AppController', 'Initializing app', { url }); const settings = await this.settingsService.initialize(); @@ -83,11 +83,11 @@ export default class AppController { } if (config.features?.continueWatchingList && config.content.some((el) => el.type === PersonalShelf.ContinueWatching)) { - await getModule(WatchHistoryController).initialize(); + await getModule(WatchHistoryController).initialize(language); } if (config.features?.favoritesList && config.content.some((el) => el.type === PersonalShelf.Favorites)) { - await getModule(FavoritesController).initialize(); + await getModule(FavoritesController).initialize(language); } return { config, settings, configSource }; diff --git a/packages/common/src/controllers/FavoritesController.ts b/packages/common/src/controllers/FavoritesController.ts index e871bcf63..9dc9bdf76 100644 --- a/packages/common/src/controllers/FavoritesController.ts +++ b/packages/common/src/controllers/FavoritesController.ts @@ -15,11 +15,11 @@ export default class FavoritesController { this.favoritesService = favoritesService; } - initialize = async () => { - await this.restoreFavorites(); + initialize = async (language: string) => { + await this.restoreFavorites(language); }; - restoreFavorites = async () => { + restoreFavorites = async (language?: string) => { const { user } = useAccountStore.getState(); const favoritesList = useConfigStore.getState().config.features?.favoritesList; @@ -27,7 +27,7 @@ export default class FavoritesController { return; } - const favorites = await this.favoritesService.getFavorites(user, favoritesList); + const favorites = await this.favoritesService.getFavorites(user, favoritesList, language); useFavoritesStore.setState({ favorites, favoritesPlaylistId: favoritesList }); }; diff --git a/packages/common/src/controllers/WatchHistoryController.ts b/packages/common/src/controllers/WatchHistoryController.ts index 708914ec1..e433bfa7f 100644 --- a/packages/common/src/controllers/WatchHistoryController.ts +++ b/packages/common/src/controllers/WatchHistoryController.ts @@ -15,11 +15,11 @@ export default class WatchHistoryController { this.watchHistoryService = watchHistoryService; } - initialize = async () => { - await this.restoreWatchHistory(); + initialize = async (language: string) => { + await this.restoreWatchHistory(language); }; - restoreWatchHistory = async () => { + restoreWatchHistory = async (language?: string) => { const { user } = useAccountStore.getState(); const continueWatchingList = useConfigStore.getState().config.features?.continueWatchingList; @@ -27,7 +27,7 @@ export default class WatchHistoryController { return; } - const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList); + const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList, language); useWatchHistoryStore.setState({ watchHistory: watchHistory.filter((item): item is WatchHistoryItem => !!item?.mediaid), diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index 6cf199d1d..0089768e2 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -46,10 +46,29 @@ export default class ApiService { return date ? parseISO(date) : undefined; }; + protected getTranslatedFields = (item: PlaylistItem, language?: string) => { + if (!language) { + return item; + } + + const defaultLanguage = env.APP_DEFAULT_LANGUAGE; + const transformedItem = { ...item }; + + if (language !== defaultLanguage) { + for (const [key, _] of Object.entries(transformedItem)) { + if (item[`${key}-${language}`]) { + transformedItem[key] = item[`${key}-${language}`]; + } + } + } + + return transformedItem; + }; + /** * Transform incoming content lists */ - protected transformContentList = (contentList: ContentList): Playlist => { + protected transformContentList = (contentList: ContentList, language: string): Playlist => { const { list, ...rest } = contentList; const playlist: Playlist = { ...rest, playlist: [] }; @@ -71,7 +90,7 @@ export default class ApiService { ...custom_params, }; - return this.transformMediaItem(playlistItem, playlist); + return this.transformMediaItem({ item: playlistItem, playlist, language }); }); return playlist; @@ -80,8 +99,8 @@ export default class ApiService { /** * Transform incoming playlists */ - protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string) => { - playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem(item, playlist)); + protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string, language?: string) => { + playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem({ item, playlist, language })); // remove the related media item (when this is a recommendations playlist) if (relatedMediaId) { @@ -95,14 +114,16 @@ export default class ApiService { * Transform incoming media items * - Parses productId into MediaOffer[] for all cleeng offers */ - transformMediaItem = (item: PlaylistItem, playlist?: Playlist) => { + transformMediaItem = ({ item, playlist, language }: { item: PlaylistItem; playlist?: Playlist; language?: string }) => { const config = ConfigStore.getState().config; const offerKeys = Object.keys(config?.integrations)[0]; const playlistLabel = playlist?.imageLabel; const mediaId = item.mediaid; + const translatedFields = this.getTranslatedFields(item, language); const transformedMediaItem = { ...item, + ...translatedFields, cardImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CARD, playlistLabel }), channelLogoImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CHANNEL_LOGO, playlistLabel }), backgroundImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.BACKGROUND }), @@ -117,7 +138,7 @@ export default class ApiService { return transformedMediaItem; }; - private transformEpisodes = (episodesRes: EpisodesRes, seasonNumber?: number) => { + private transformEpisodes = (episodesRes: EpisodesRes, language: string, seasonNumber?: number) => { const { episodes, page, page_limit, total } = episodesRes; // Adding images and keys for media items @@ -125,7 +146,7 @@ export default class ApiService { episodes: episodes .filter((el) => el.media_item) .map((el) => ({ - ...this.transformMediaItem(el.media_item as PlaylistItem), + ...this.transformMediaItem({ item: el.media_item as PlaylistItem, language }), seasonNumber: seasonNumber?.toString() || el.season_number?.toString() || '', episodeNumber: String(el.episode_number), })), @@ -136,7 +157,17 @@ export default class ApiService { /** * Get watchlist by playlistId */ - getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise => { + getMediaByWatchlist = async ({ + playlistId, + mediaIds, + token, + language, + }: { + playlistId: string; + mediaIds: string[]; + token?: string; + language?: string; + }): Promise => { if (!mediaIds?.length) { return []; } @@ -148,16 +179,23 @@ export default class ApiService { if (!data) throw new Error(`The data was not found using the watchlist ${playlistId}`); - return (data.playlist || []).map((item) => this.transformMediaItem(item)); + return (data.playlist || []).map((item) => this.transformMediaItem({ item, language })); }; /** * Get media by id - * @param {string} id - * @param {string} [token] - * @param {string} [drmPolicyId] */ - getMediaById = async (id: string, token?: string, drmPolicyId?: string): Promise => { + getMediaById = async ({ + id, + token, + drmPolicyId, + language, + }: { + id: string; + token?: string; + drmPolicyId?: string; + language?: string; + }): Promise => { const pathname = drmPolicyId ? `/v2/media/${id}/drm/${drmPolicyId}` : `/v2/media/${id}`; const url = createURL(`${env.APP_API_BASE_URL}${pathname}`, { token }); const response = await fetch(url); @@ -166,7 +204,7 @@ export default class ApiService { if (!mediaItem) throw new Error('MediaItem not found'); - return this.transformMediaItem(mediaItem); + return this.transformMediaItem({ item: mediaItem, language }); }; /** @@ -205,11 +243,13 @@ export default class ApiService { pageOffset, pageLimit = PAGE_LIMIT, afterId, + language, }: { seriesId: string | undefined; pageOffset?: number; pageLimit?: number; afterId?: string; + language: string; }): Promise => { if (!seriesId) { throw new Error('Series ID is required'); @@ -225,7 +265,7 @@ export default class ApiService { const response = await fetch(url); const episodesResponse = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesResponse); + return this.transformEpisodes(episodesResponse, language); }; /** @@ -236,8 +276,10 @@ export default class ApiService { seasonNumber, pageOffset, pageLimit = PAGE_LIMIT, + language, }: { seriesId: string | undefined; + language: string; seasonNumber: number; pageOffset?: number; pageLimit?: number; @@ -252,7 +294,7 @@ export default class ApiService { const response = await fetch(url); const episodesRes = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesRes, seasonNumber); + return this.transformEpisodes(episodesRes, language, seasonNumber); }; getAdSchedule = async (id: string | undefined | null): Promise => { @@ -269,7 +311,7 @@ export default class ApiService { /** * Get playlist by id */ - getPlaylistById = async (id?: string, params: GetPlaylistParams = {}): Promise => { + getPlaylistById = async (id?: string, params: GetPlaylistParams = {}, language: string = env.APP_DEFAULT_LANGUAGE): Promise => { if (!id) { return undefined; } @@ -279,10 +321,10 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as Playlist; - return this.transformPlaylist(data, params.related_media_id); + return this.transformPlaylist(data, params.related_media_id, language); }; - getContentList = async ({ id, siteId }: { id: string | undefined; siteId: string }): Promise => { + getContentList = async ({ id, siteId, language }: { id: string | undefined; siteId: string; language: string }): Promise => { if (!id || !siteId) { throw new Error('List ID and Site ID are required'); } @@ -292,7 +334,7 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as ContentList; - return this.transformContentList(data); + return this.transformContentList(data, language); }; getContentSearch = async ({ siteId, params }: { siteId: string; params: GetContentSearchParams }) => { diff --git a/packages/common/src/services/FavoriteService.ts b/packages/common/src/services/FavoriteService.ts index b35be09b5..b6eb968f6 100644 --- a/packages/common/src/services/FavoriteService.ts +++ b/packages/common/src/services/FavoriteService.ts @@ -57,7 +57,7 @@ export default class FavoriteService { return this.validateFavorites(favorites); } - getFavorites = async (user: Customer | null, favoritesList: string) => { + getFavorites = async (user: Customer | null, favoritesList: string, language?: string) => { const savedItems = user ? await this.getFavoritesFromAccount(user) : await this.getFavoritesFromStorage(); const mediaIds = savedItems.map(({ mediaid }) => mediaid); @@ -66,7 +66,7 @@ export default class FavoriteService { } try { - const playlistItems = await this.apiService.getMediaByWatchlist(favoritesList, mediaIds); + const playlistItems = await this.apiService.getMediaByWatchlist({ playlistId: favoritesList, mediaIds, language }); return (playlistItems || []).map((item) => this.createFavorite(item)); } catch (error: unknown) { diff --git a/packages/common/src/services/WatchHistoryService.ts b/packages/common/src/services/WatchHistoryService.ts index 80ec9d56a..c8beaa396 100644 --- a/packages/common/src/services/WatchHistoryService.ts +++ b/packages/common/src/services/WatchHistoryService.ts @@ -39,22 +39,25 @@ export default class WatchHistoryService { } // Retrieve watch history media items info using a provided watch list - protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[]): Promise> => { - const watchHistoryItems = await this.apiService.getMediaByWatchlist(continueWatchingList, ids); + protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[], language?: string): Promise> => { + const watchHistoryItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: ids, language }); const watchHistoryItemsDict = Object.fromEntries((watchHistoryItems || []).map((item) => [item.mediaid, item])); return watchHistoryItemsDict; }; // We store separate episodes in the watch history and to show series card in the Continue Watching shelf we need to get their parent media items - protected getWatchHistorySeriesItems = async (continueWatchingList: string, ids: string[]): Promise> => { + protected getWatchHistorySeriesItems = async ( + continueWatchingList: string, + ids: string[], + language?: string, + ): Promise> => { const mediaWithSeries = await this.apiService.getSeriesByMediaIds(ids); const seriesIds = Object.keys(mediaWithSeries || {}) .map((key) => mediaWithSeries?.[key]?.[0]?.series_id) .filter(Boolean) as string[]; const uniqueSerieIds = [...new Set(seriesIds)]; - - const seriesItems = await this.apiService.getMediaByWatchlist(continueWatchingList, uniqueSerieIds); + const seriesItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: uniqueSerieIds, language }); const seriesItemsDict = Object.keys(mediaWithSeries || {}).reduce((acc, key) => { const seriesItemId = mediaWithSeries?.[key]?.[0]?.series_id; if (seriesItemId) { @@ -86,7 +89,7 @@ export default class WatchHistoryService { return this.validateWatchHistory(history); } - getWatchHistory = async (user: Customer | null, continueWatchingList: string) => { + getWatchHistory = async (user: Customer | null, continueWatchingList: string, language?: string) => { const savedItems = user ? await this.getWatchHistoryFromAccount(user) : await this.getWatchHistoryFromStorage(); // When item is an episode of the new flow -> show the card as a series one, but keep episode to redirect in a right way @@ -97,8 +100,8 @@ export default class WatchHistoryService { } try { - const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids); - const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids); + const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids, language); + const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids, language); return savedItems .map((item) => { diff --git a/packages/hooks-react/src/series/useEpisodes.ts b/packages/hooks-react/src/series/useEpisodes.ts index 7d91fa9e2..76a29aa93 100644 --- a/packages/hooks-react/src/series/useEpisodes.ts +++ b/packages/hooks-react/src/series/useEpisodes.ts @@ -4,6 +4,7 @@ import type { Pagination } from '@jwp/ott-common/types/pagination'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; const getNextPageParam = (pagination: Pagination) => { const { page, page_limit, total } = pagination; @@ -28,22 +29,26 @@ export const useEpisodes = ( } => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { data, fetchNextPage, isLoading, hasNextPage = false, } = useInfiniteQuery( - [seriesId, seasonNumber], + [seriesId, seasonNumber, language], async ({ pageParam = 0 }) => { if (Number(seasonNumber)) { // Get episodes from a selected season using pagination - const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam }); + const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam, language }); return { pagination: season.pagination, episodes: season.episodes }; } else { // Get episodes from a selected series using pagination - const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam }); + const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam, language }); return data; } }, diff --git a/packages/hooks-react/src/series/useNextEpisode.ts b/packages/hooks-react/src/series/useNextEpisode.ts index 2a33d573c..ea97a0c3f 100644 --- a/packages/hooks-react/src/series/useNextEpisode.ts +++ b/packages/hooks-react/src/series/useNextEpisode.ts @@ -3,14 +3,19 @@ import type { Series } from '@jwp/ott-common/types/series'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; export const useNextEpisode = ({ series, episodeId }: { series: Series | undefined; episodeId: string | undefined }) => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { isLoading, data } = useQuery( - ['next-episode', series?.series_id, episodeId], + ['next-episode', series?.series_id, episodeId, language], async () => { - const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId }); + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId, language }); return item?.episodes?.[0]; }, diff --git a/packages/hooks-react/src/useBootstrapApp.ts b/packages/hooks-react/src/useBootstrapApp.ts index 6c366dc21..ad85e2564 100644 --- a/packages/hooks-react/src/useBootstrapApp.ts +++ b/packages/hooks-react/src/useBootstrapApp.ts @@ -6,6 +6,7 @@ import AppController from '@jwp/ott-common/src/controllers/AppController'; import type { AppError } from '@jwp/ott-common/src/utils/error'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; import { logDebug } from '@jwp/ott-common/src/logger'; +import { useTranslation } from 'react-i18next'; const applicationController = getModule(AppController); @@ -19,11 +20,13 @@ export type OnReadyCallback = (config: Config | undefined) => void; export const useBootstrapApp = (url: string, onReady: OnReadyCallback) => { const queryClient = useQueryClient(); + const { i18n } = useTranslation(); + const refreshEntitlements = () => queryClient.invalidateQueries({ queryKey: ['entitlements'] }); const { data, isLoading, error, isSuccess, refetch } = useQuery( 'config-init', - () => applicationController.initializeApp(url, refreshEntitlements), + () => applicationController.initializeApp(url, i18n.language, refreshEntitlements), { refetchInterval: false, retry: 1, diff --git a/packages/hooks-react/src/useMedia.ts b/packages/hooks-react/src/useMedia.ts index a3cd68aa3..78de95e3d 100644 --- a/packages/hooks-react/src/useMedia.ts +++ b/packages/hooks-react/src/useMedia.ts @@ -3,13 +3,18 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; +import { useTranslation } from 'react-i18next'; export type UseMediaResult = UseBaseQueryResult; export default function useMedia(mediaId: string, enabled: boolean = true): UseMediaResult { const apiService = getModule(ApiService); - return useQuery(['media', mediaId], () => apiService.getMediaById(mediaId), { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + + return useQuery(['media', mediaId, language], () => apiService.getMediaById({ id: mediaId, language }), { enabled: !!mediaId && enabled, refetchInterval: (data, _) => { if (!data) return false; diff --git a/packages/hooks-react/src/usePlaylist.ts b/packages/hooks-react/src/usePlaylist.ts index 5e8ed77b5..6768653f7 100644 --- a/packages/hooks-react/src/usePlaylist.ts +++ b/packages/hooks-react/src/usePlaylist.ts @@ -9,6 +9,7 @@ import type { ApiError } from '@jwp/ott-common/src/utils/api'; import type { AppMenuType } from '@jwp/ott-common/types/config'; import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useTranslation } from 'react-i18next'; const placeholderData = generatePlaylistPlaceholder(30); @@ -20,6 +21,7 @@ export const getPlaylistQueryOptions = ({ usePlaceholderData, params = {}, queryClient, + language, }: { type: AppMenuType; contentId: string | undefined; @@ -28,15 +30,16 @@ export const getPlaylistQueryOptions = ({ queryClient: QueryClient; usePlaceholderData?: boolean; params?: GetPlaylistParams; + language: string; }) => { const apiService = getModule(ApiService); return { enabled: !!contentId && enabled, - queryKey: ['playlist', type, contentId, params], + queryKey: ['playlist', type, contentId, params, language], queryFn: async () => { if (type === APP_CONFIG_ITEM_TYPE.playlist) { - const playlist = await apiService.getPlaylistById(contentId, params); + const playlist = await apiService.getPlaylistById(contentId, params, language); // This pre-caches all playlist items and makes navigating a lot faster. playlist?.playlist?.forEach((playlistItem) => { @@ -45,7 +48,7 @@ export const getPlaylistQueryOptions = ({ return playlist; } else if (type === APP_CONFIG_ITEM_TYPE.content_list) { - const contentList = await apiService.getContentList({ siteId, id: contentId }); + const contentList = await apiService.getContentList({ siteId, id: contentId, language }); return contentList; } @@ -69,10 +72,14 @@ export default function usePlaylist( usePlaceholderData: boolean = true, type: AppMenuType = APP_CONFIG_ITEM_TYPE.playlist, ) { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const queryClient = useQueryClient(); const siteId = useConfigStore((state) => state.config.siteId); - const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData }); + const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData, language }); return useQuery(queryOptions); } diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts index 14e658246..b3824ac9b 100644 --- a/packages/hooks-react/src/usePlaylists.ts +++ b/packages/hooks-react/src/usePlaylists.ts @@ -5,6 +5,7 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import type { Content, AppContentType, AppMenuType } from '@jwp/ott-common/types/config'; import type { Playlist } from '@jwp/ott-common/types/playlist'; import { useQueries, useQueryClient } from 'react-query'; +import { useTranslation } from 'react-i18next'; import { getPlaylistQueryOptions } from './usePlaylist'; @@ -25,6 +26,9 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef const favorites = useFavoritesStore((state) => state.getPlaylist()); const watchHistory = useWatchHistoryStore((state) => state.getPlaylist()); + // Determine currently selected language + const { i18n } = useTranslation(); + const playlistQueries = useQueries( content.map(({ contentId, type }, index) => { if (isPlaylistType(type)) { @@ -36,6 +40,7 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef queryClient, usePlaceholderData: true, params: { page_limit }, + language: i18n.language, }); } diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index c75649b95..f034e3378 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -7,7 +7,9 @@ import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); - const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => apiService.getMediaById(item.mediaid, token, drmPolicyId)); + const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => + apiService.getMediaById({ id: item.mediaid, token, drmPolicyId }), + ); const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], diff --git a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx index 1513d9089..15ce22af5 100644 --- a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx +++ b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx @@ -2,6 +2,9 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import LanguageMenu from '../../components/LanguageMenu/LanguageMenu'; @@ -10,11 +13,16 @@ const HeaderLanguageSwitcher = () => { const supportedLanguages = useConfigStore((state) => state.supportedLanguages); const languageMenuOpen = useUIStore((state) => state.languageMenuOpen); + const favoritesController = getModule(FavoritesController); + const watchlistController = getModule(WatchHistoryController); + const openLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: true }), []); const closeLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: false }), []); const languageClickHandler = (code: string) => { i18n.changeLanguage(code); + favoritesController.restoreFavorites(code); + watchlistController.restoreWatchHistory(code); }; const currentLanguage = useMemo(() => supportedLanguages.find(({ code }) => code === i18n.language), [i18n.language, supportedLanguages]); diff --git a/packages/ui-react/src/containers/Layout/Layout.test.tsx b/packages/ui-react/src/containers/Layout/Layout.test.tsx index 6ba246c09..bccd5de3f 100644 --- a/packages/ui-react/src/containers/Layout/Layout.test.tsx +++ b/packages/ui-react/src/containers/Layout/Layout.test.tsx @@ -3,6 +3,8 @@ import { axe } from 'vitest-axe'; import AccountController from '@jwp/ott-common/src/controllers/AccountController'; import { mockService } from '@jwp/ott-common/test/mockService'; import { DEFAULT_FEATURES } from '@jwp/ott-common/src/constants'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import { renderWithRouter } from '../../../test/utils'; @@ -11,6 +13,8 @@ import Layout from './Layout'; describe('', () => { beforeEach(() => { mockService(AccountController, { getFeatures: () => DEFAULT_FEATURES }); + mockService(FavoritesController, {}); + mockService(WatchHistoryController, {}); }); test('renders layout', () => { From ddedb6b6f636c720f0ba016b2fa07b41e964fe8e Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Thu, 12 Sep 2024 16:58:58 +0200 Subject: [PATCH 09/12] fix(series): first episode switching per season --- .../hooks-react/src/series/useFirstEpisode.ts | 24 +++++++++++ .../mediaScreens/MediaSeries/MediaSeries.tsx | 41 ++++++++++--------- 2 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 packages/hooks-react/src/series/useFirstEpisode.ts diff --git a/packages/hooks-react/src/series/useFirstEpisode.ts b/packages/hooks-react/src/series/useFirstEpisode.ts new file mode 100644 index 000000000..15d16a509 --- /dev/null +++ b/packages/hooks-react/src/series/useFirstEpisode.ts @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; +import type { Series } from '@jwp/ott-common/types/series'; +import ApiService from '@jwp/ott-common/src/services/ApiService'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; + +export const useFirstEpisode = ({ series }: { series: Series | undefined }) => { + const apiService = getModule(ApiService); + + const { isLoading, data } = useQuery( + ['first-episode', series?.series_id], + async () => { + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1 }); + + return item?.episodes?.[0]; + }, + { staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: !!series?.series_id }, + ); + + return { + isLoading, + data, + }; +}; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx index 46beef251..37c617bc8 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx @@ -22,6 +22,7 @@ import { useSeriesLookup } from '@jwp/ott-hooks-react/src/series/useSeriesLookup import { useNextEpisode } from '@jwp/ott-hooks-react/src/series/useNextEpisode'; import PlayTrailer from '@jwp/ott-theme/assets/icons/play_trailer.svg?react'; import useBreakpoint, { Breakpoint } from '@jwp/ott-ui-react/src/hooks/useBreakpoint'; +import { useFirstEpisode } from '@jwp/ott-hooks-react/src/series/useFirstEpisode'; import type { ScreenComponent } from '../../../../../types/screens'; import ErrorPage from '../../../../components/ErrorPage/ErrorPage'; @@ -54,7 +55,8 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { const { isLoading: isSeriesDataLoading, data: series, error: seriesError } = useSeries(seriesId); const { isLoading: isEpisodeLoading, data: episode } = useMedia(episodeId || ''); const { isLoading: isTrailerLoading, data: trailerItem } = useMedia(episode?.trailerId || ''); - const { data: episodeInSeries, isLoading: isSeriesDictionaryLoading } = useSeriesLookup(episode?.mediaid); + const { isLoading: isSeriesDictionaryLoading, data: episodeInSeries } = useSeriesLookup(episodeId || undefined); + const { isLoading: isFirstEpisodeLoading, data: firstEpisode } = useFirstEpisode({ series }); // Whether we show series or episode information const selectedItem = (episode || seriesMedia) as PlaylistItem; @@ -76,7 +78,6 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { hasNextPage: hasNextEpisodesPage, } = useEpisodes(seriesId, seasonFilter, { enabled: seasonFilter !== undefined && !!series }); - const firstEpisode = useMemo(() => episodes?.[0]?.episodes?.[0], [episodes]); const episodeMetadata = useMemo( () => episodeInSeries && { @@ -162,22 +163,24 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { } }, [episodeMetadata, seasonFilter, isSeriesDataLoading, isEpisodeLoading, isSeriesDictionaryLoading, filters, episodeId]); + // UI + const playEpisode = episode || firstEpisode; + const isLoading = isSeriesDataLoading || isSeriesDictionaryLoading || isEpisodeLoading || isFirstEpisodeLoading; + const startWatchingButton = useMemo( - () => ( - { - setSearchParams({ ...searchParams, e: (episode || firstEpisode).mediaid, r: feedId || '', play: '1' }, { replace: true }); - }} - /> - ), - [episodeId, episode, firstEpisode, setSearchParams, searchParams, feedId], + () => + playEpisode && ( + { + setSearchParams({ ...searchParams, e: playEpisode.mediaid, r: feedId || '', play: '1' }, { replace: true }); + }} + /> + ), + [episodeId, playEpisode, setSearchParams, searchParams, feedId], ); - // UI - const isLoading = isSeriesDataLoading || isSeriesDictionaryLoading || isEpisodeLoading; - if (isLoading) return ; // Legacy series is used @@ -187,7 +190,7 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { return ; } - if (!seriesMedia || !series) return ; + if (!seriesMedia || !series || !playEpisode) return ; const pageTitle = `${selectedItem.title} - ${siteName}`; const canonicalUrl = `${window.location.origin}${mediaURL({ media: seriesMedia, episodeId: episode?.mediaid })}`; @@ -274,10 +277,10 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { hasMore={hasNextEpisodesPage} loadMore={fetchNextEpisodes} player={ - inlineLayout && (episode || firstEpisode) ? ( + inlineLayout ? ( = ({ data: seriesMedia }) => { Date: Thu, 19 Sep 2024 15:04:27 +0200 Subject: [PATCH 10/12] fix: card grid rendering previous items (#613) * fix: card grid rendering previous items * chore: remove unused code --- .../src/components/CardGrid/CardGrid.tsx | 40 +++++++++++-------- .../src/components/LayoutGrid/LayoutGrid.tsx | 11 +++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/ui-react/src/components/CardGrid/CardGrid.tsx b/packages/ui-react/src/components/CardGrid/CardGrid.tsx index 868628b92..e900a7fe1 100644 --- a/packages/ui-react/src/components/CardGrid/CardGrid.tsx +++ b/packages/ui-react/src/components/CardGrid/CardGrid.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import classNames from 'classnames'; import InfiniteScroll from 'react-infinite-scroller'; import type { Playlist, PlaylistItem } from '@jwp/ott-common/types/playlist'; @@ -46,6 +46,8 @@ export type CardGridProps = { getUrl: (item: PlaylistItem) => string; }; +const getCellKey = (item: PlaylistItem) => item.mediaid; + function CardGrid({ playlist, watchHistory, @@ -75,6 +77,25 @@ function CardGrid({ setRowCount(INITIAL_ROW_COUNT); }, [playlist.feedid]); + const renderCell = useCallback( + (playlistItem: PlaylistItem, tabIndex: number) => ( + onCardHover(playlistItem) : undefined} + loading={isLoading} + isCurrent={currentCardItem && currentCardItem.mediaid === playlistItem.mediaid} + currentLabel={currentCardLabel} + isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, playlistItem)} + posterAspect={posterAspect} + item={playlistItem} + headingLevel={headingLevel} + /> + ), + [accessModel, currentCardItem, currentCardLabel, getUrl, hasSubscription, headingLevel, isLoading, isLoggedIn, onCardHover, posterAspect, watchHistory], + ); + return ( ( - onCardHover(playlistItem) : undefined} - loading={isLoading} - isCurrent={currentCardItem && currentCardItem.mediaid === playlistItem.mediaid} - currentLabel={currentCardLabel} - isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, playlistItem)} - posterAspect={posterAspect} - item={playlistItem} - headingLevel={headingLevel} - /> - )} + renderCell={renderCell} + getCellKey={getCellKey} /> ); diff --git a/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx b/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx index 385a9abab..cbf9bfcf4 100644 --- a/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx +++ b/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx @@ -1,6 +1,6 @@ import { throttle } from '@jwp/ott-common/src/utils/common'; import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styles from './LayoutGrid.module.scss'; @@ -9,6 +9,7 @@ type Props = { columnCount: number; data: Item[]; renderCell: (item: Item, tabIndex: number) => JSX.Element; + getCellKey: (item: Item) => string; }; const scrollIntoViewThrottled = throttle(function (focusedElement: HTMLElement) { @@ -16,7 +17,7 @@ const scrollIntoViewThrottled = throttle(function (focusedElement: HTMLElement) }, 300); // Keyboard-accessible grid layout, with focus management -const LayoutGrid = ({ className, columnCount, data, renderCell }: Props) => { +const LayoutGrid = ({ className, columnCount, data, renderCell, getCellKey }: Props) => { const [currentRowIndex, setCurrentRowIndex] = useState(0); const [currentColumnIndex, setCurrentColumnIndex] = useState(0); const gridRef = useRef(null); @@ -110,6 +111,8 @@ const LayoutGrid = ({ className, columnCount, data, renderC // eslint-disable-next-line react-hooks/exhaustive-deps }, [columnCount]); + const gridCellStyle = useMemo(() => ({ width: `${Math.round(100 / columnCount)}%` }), [columnCount]); + return (
    {Array.from({ length: rowCount }).map((_, rowIndex) => ( @@ -118,10 +121,10 @@ const LayoutGrid = ({ className, columnCount, data, renderC
    {renderCell(item, currentRowIndex === rowIndex && currentColumnIndex === columnIndex ? 0 : -1)}
    From 392ee47bb6516c4efadbd2b0ed979bff9e806d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKire?= Date: Thu, 19 Sep 2024 15:21:35 +0200 Subject: [PATCH 11/12] fix: make language as optional, to fix lint --- packages/common/src/services/ApiService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index a731a1375..0f36588c1 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -138,7 +138,7 @@ export default class ApiService { return transformedMediaItem; }; - private transformEpisodes = (episodesRes: EpisodesRes, language: string, seasonNumber?: number) => { + private transformEpisodes = (episodesRes: EpisodesRes, language?: string, seasonNumber?: number) => { const { episodes, page, page_limit, total } = episodesRes; // Adding images and keys for media items @@ -276,7 +276,7 @@ export default class ApiService { pageOffset?: number; pageLimit?: number; afterId?: string; - language: string; + language?: string; }): Promise => { if (!seriesId) { throw new Error('Series ID is required'); From c27a1e69c360d9ebe9abcc9cf2e8bb3a088516aa Mon Sep 17 00:00:00 2001 From: Carina Dragan Date: Thu, 19 Sep 2024 16:22:47 +0300 Subject: [PATCH 12/12] feat(i18n): fix lint error --- packages/hooks-react/src/series/useFirstEpisode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/hooks-react/src/series/useFirstEpisode.ts b/packages/hooks-react/src/series/useFirstEpisode.ts index 15d16a509..74ff010eb 100644 --- a/packages/hooks-react/src/series/useFirstEpisode.ts +++ b/packages/hooks-react/src/series/useFirstEpisode.ts @@ -3,14 +3,16 @@ import type { Series } from '@jwp/ott-common/types/series'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; export const useFirstEpisode = ({ series }: { series: Series | undefined }) => { const apiService = getModule(ApiService); + const { i18n } = useTranslation(); const { isLoading, data } = useQuery( ['first-episode', series?.series_id], async () => { - const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1 }); + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, language: i18n.language }); return item?.episodes?.[0]; },