diff --git a/.drone.yml b/.drone.yml index cec503c6..88023b8d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -73,7 +73,7 @@ steps: from_secret: AWS_DEFAULT_REGION AWS_MAX_ATTEMPTS: 10 commands: - - aws s3 sync build s3://insports-auth --delete + - aws s3 sync build_auth s3://insports-auth --delete - aws cloudfront create-invalidation --distribution-id EERIKX9X2SRPJ --paths "/*" depends_on: - make-auth @@ -88,6 +88,8 @@ steps: - eval $(ssh-agent -s) - echo -n "$SSH_KEY_AUTH" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh && chmod 700 ~/.ssh + - ssh-keyscan auth.insports.tv >> ~/.ssh/known_hosts + - rsync -v -r -C build_auth/ ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/ depends_on: - make-auth @@ -681,3 +683,72 @@ steps: - rsync -v -r -C build_auth/clients/* ubuntu@auth.test.insports.tv:/home/ubuntu/ott-auth/src/frontend/templates - aws s3 sync build_auth s3://auth-insports-test --delete - aws cloudfront create-invalidation --distribution-id E10YI3RFOZZDLZ --paths "/*" + + +--- +kind: pipeline +type: docker +name: deploy auth prod + +concurrency: + limit: 1 + +platform: + os: linux + arch: amd64 + +trigger: + ref: + - refs/heads/auth + +steps: + - name: npm-install + image: node:16-alpine + environment: + REACT_APP_STRIPE_PK: + from_secret: REACT_APP_STRIPE_PK + commands: + - apk add --no-cache make + - npm install --legacy-peer-deps + + - name: make-auth + image: node:16-alpine + environment: + REACT_APP_STRIPE_PK: + from_secret: REACT_APP_STRIPE_PK + commands: + - apk add --no-cache make + - make auth-production-build + depends_on: + - npm-install + + - name: deploy-S3-auth + image: amazon/aws-cli:latest + environment: + AWS_ACCESS_KEY_ID: + from_secret: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY: + from_secret: AWS_SECRET_ACCESS_KEY + AWS_DEFAULT_REGION: + from_secret: AWS_DEFAULT_REGION + AWS_MAX_ATTEMPTS: 10 + commands: + - aws s3 sync build_auth s3://insports-auth --delete + - aws cloudfront create-invalidation --distribution-id EERIKX9X2SRPJ --paths "/*" + depends_on: + - make-auth + + - name: deploy-old-auth-server + image: node:16-alpine + environment: + SSH_KEY_AUTH: + from_secret: SSH_KEY_AUTH + commands: + - apk add --no-cache openssh-client rsync + - eval $(ssh-agent -s) + - echo -n "$SSH_KEY_AUTH" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh && chmod 700 ~/.ssh + - ssh-keyscan auth.insports.tv >> ~/.ssh/known_hosts + - rsync -v -r -C build_auth/ ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/ + depends_on: + - make-auth \ No newline at end of file diff --git a/src/components/AccessTimer/index.tsx b/src/components/AccessTimer/index.tsx new file mode 100644 index 00000000..faebddbe --- /dev/null +++ b/src/components/AccessTimer/index.tsx @@ -0,0 +1,128 @@ +import { + useEffect, + useState, + RefObject, +} from 'react' + +import { T9n } from 'features/T9n' +import { Icon } from 'features/Icon' +import { useAuthStore } from 'features/AuthStore' + +import { + AccessTimerContainer, + PreviewInfo, + SignInBtn, + SignText, + Timer, + TimerContainer, +} from './styled' + +import { SimplePopup } from '../SimplePopup' +import { secondsToHms } from '../../helpers' +import { getViewMatchDuration } from '../../requests/getViewMatchDuration' +import { usePageParams } from '../../hooks' +import { getUserInfo } from '../../requests' + +type AccessTimerType = { + access: boolean, + isFullscreen: boolean, + onFullscreenClick: () => void, + playing: boolean, + videoRef?: RefObject | null, +} + +const ACCESS_TIME = 60 + +export const AccessTimer = ({ + access, + isFullscreen, + onFullscreenClick, + playing, + videoRef, +}: AccessTimerType) => { + const { + logout, + setUserInfo, + userInfo, + } = useAuthStore() + + const { profileId, sportType } = usePageParams() + + const [timeIsFinished, setTimeIsFinished] = useState(true) + const [time, setTime] = useState(ACCESS_TIME) + const isTimeExpired = time <= 0 || !access + + useEffect(() => { + if (isTimeExpired) { + document.pictureInPictureEnabled && document.exitPictureInPicture() + setTimeIsFinished(true) + videoRef?.current?.pause() + setTime(0) + if (isFullscreen) onFullscreenClick() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [access, playing, profileId]) + + useEffect(() => { + let stopWatch: ReturnType | null = null + if (playing) { + stopWatch = setInterval(() => { + setTime((prev) => prev - 1) + }, 1000) + } else { + stopWatch && clearInterval(stopWatch) + } + return () => { + stopWatch && clearInterval(stopWatch) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playing, profileId]) + + useEffect(() => { + (async () => { + setTime(ACCESS_TIME) + const updatedUserInfo = await getUserInfo() + setUserInfo(updatedUserInfo) + const timeViewed = await getViewMatchDuration({ + matchId: profileId, + sportType, + userId: Number(updatedUserInfo?.email), + }) + setTime(isTimeExpired ? 0 : (ACCESS_TIME - Number(timeViewed.duration))) + })() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [profileId]) + + return ( + <> + {(!userInfo || Number(userInfo?.email) < 0) && access && time > 0 + ? ( + + + + {secondsToHms(time)}  + + + + + + + logout('saveToken')}> + + + + ) + : ( + logout('saveToken')} + icon={} + /> + )} + + ) +} diff --git a/src/components/AccessTimer/styled.tsx b/src/components/AccessTimer/styled.tsx new file mode 100644 index 00000000..fb2e24c7 --- /dev/null +++ b/src/components/AccessTimer/styled.tsx @@ -0,0 +1,124 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config' + +import { ButtonSolid } from 'features/Common' + +export const AccessTimerContainer = styled.div<{isFullscreen?: boolean}>` + display: flex; + align-items: center; + + position: absolute; + left: 50%; + bottom: 5.7rem; + transform: translateX(-50%); + background-color: #333333; + border-radius: 5px; + padding: 20px 50px; + + gap: 40px; + + ${isMobileDevice + ? css` + padding: 9px 7px 9px 15px; + bottom: 6.5rem; + + @media screen and (orientation: landscape) { + max-width: 95%; + bottom: 4rem; + } + ` + : ''}; + + ${({ isFullscreen }) => isFullscreen && css` + ${isMobileDevice + ? css` + padding: 9px 7px 9px 15px; + bottom: 12rem; + + @media screen and (orientation: landscape) { + max-width: 95%; + bottom: 6rem; + } + ` + : ''}; + `} +` + +export const SignInBtn = styled(ButtonSolid)` + width: 246px; + border-radius: 5px; + height: 50px; + font-weight: 600; + font-size: 20px; + white-space: nowrap; + padding: 0 35px; + + ${isMobileDevice + ? css` + font-size: 12px; + white-space: nowrap; + padding: 0px 16px; + width: 140px; + height: 30px; + + @media screen and (orientation: landscape) { + font-size: 12px; + padding: 0px 8px; + } + ` + : ''}; + +` + +export const TimerContainer = styled.div` + color: white; + font-weight: 700; + font-size: 20px; + line-height: 24px; + white-space: nowrap; + max-width: 215px; + + ${isMobileDevice + ? css` + font-size: 12px; + line-height: 28px; + ` + : ''}; +` + +export const PreviewInfo = styled.div` + display: flex; + line-height: 24px; + + ${isMobileDevice + ? css` + line-height: 14px; + ` + : ''}; +` + +export const Timer = styled.div` + min-width: 67px; + + ${isMobileDevice + ? css` + min-width: 40px; + ` + : ''}; +` + +export const SignText = styled.span` + display: block; + font-size: 16px; + line-height: 20px; + font-weight: 400; + white-space: break-spaces; + + ${isMobileDevice + ? css` + font-size: 9px; + line-height: 14px + ` + : ''}; +` diff --git a/src/components/ItemInfo/ItemInfo.tsx b/src/components/ItemInfo/ItemInfo.tsx index 0bc42ffc..3dc2af82 100644 --- a/src/components/ItemInfo/ItemInfo.tsx +++ b/src/components/ItemInfo/ItemInfo.tsx @@ -13,7 +13,6 @@ type ItemInfoType = { onClick: (val: any) => void, type: ProfileTypes, } - export const ItemInfo = ({ active, id, diff --git a/src/components/PictureInPicture/PiP.tsx b/src/components/PictureInPicture/PiP.tsx index 30f8661e..8e39fe0b 100644 --- a/src/components/PictureInPicture/PiP.tsx +++ b/src/components/PictureInPicture/PiP.tsx @@ -7,6 +7,7 @@ import { import styled from 'styled-components/macro' import { Icon } from 'features/Icon' +import { useAuthStore } from 'features/AuthStore' const PipWrapper = styled.div` cursor: pointer; @@ -21,6 +22,7 @@ type PipProps = { } export const PiP = memo(({ isPlaying, videoRef }: PipProps) => { + const { user } = useAuthStore() const togglePip = async () => { try { if ( @@ -43,11 +45,12 @@ export const PiP = memo(({ isPlaying, videoRef }: PipProps) => { && videoRef.current !== document.pictureInPictureElement && videoRef.current?.hidden === false && isPlaying + && user ) { await videoRef.current?.requestPictureInPicture() } }) - }, [videoRef, isPlaying]) + }, [videoRef, isPlaying, user]) return ( diff --git a/src/components/SimplePopup/index.tsx b/src/components/SimplePopup/index.tsx new file mode 100644 index 00000000..1ad02429 --- /dev/null +++ b/src/components/SimplePopup/index.tsx @@ -0,0 +1,65 @@ +import type { ReactNode } from 'react' + +import { T9n } from 'features/T9n' + +import { + Header, + Footer, + ScBody, + Modal, + ScApplyButton, + ScHeaderTitle, + ScText, + Wrapper, +} from './styled' + +type Props = { + buttonName?: string, + headerName?: string, + icon?: ReactNode, + isModalOpen: boolean, + mainText: string, + onHandle?: () => void, + withCloseButton: boolean, +} + +export const SimplePopup = (props: Props) => { + const { + buttonName, + headerName, + icon, + isModalOpen, + mainText, + onHandle, + withCloseButton, + } = props + + return ( + + +
+ {icon} + + + +
+ + + + + + {buttonName + && ( +
+ + + +
+ )} +
+
+ ) +} diff --git a/src/components/SimplePopup/styled.tsx b/src/components/SimplePopup/styled.tsx new file mode 100644 index 00000000..832eea4a --- /dev/null +++ b/src/components/SimplePopup/styled.tsx @@ -0,0 +1,146 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config' + +import { ModalWindow } from 'features/Modal/styled' +import { + ApplyButton, + Body, + Modal as BaseModal, + HeaderTitle, +} from 'features/AuthServiceApp/components/RegisterPopup/styled' +import { Header as BaseHeader } from 'features/PopupComponents' +import { client } from 'features/AuthServiceApp/config/clients/index' + +export const Modal = styled(BaseModal)` + + + ${ModalWindow} { + width: 705px; + height: 472px; + + padding: 60px 100px; + min-height: auto; + + ${isMobileDevice + ? css` + max-width: 95vw; + padding: 50px 20px 80px 20px; + + @media screen and (orientation: landscape) { + max-height: 80vh; + max-width: 95vw; + padding: 24px 100px 60px 100px; + } + ` + : ''}; + } +` + +export const Wrapper = styled.div` + gap: 30px; + + ${isMobileDevice + ? css` + max-height: 80vh; + gap: 10px; + ` + : ''}; +` + +export const ScHeaderTitle = styled(HeaderTitle)` + text-align: center; + font-weight: 700; + font-size: 34px; + line-height: 34px; + + ${isMobileDevice + ? css` + font-size: 20px; + line-height: 24px; + + @media screen and (orientation: landscape) { + font-size: 17px; + } + ` + : ''}; +` + +export const ScBody = styled(Body)` + padding: 16px 0 0 0; + + ${isMobileDevice + ? css` + @media screen and (orientation: landscape) { + max-width: 491px; + } + ` + : ''}; +` + +export const ScText = styled.span` + text-align: center; + + ${isMobileDevice + ? css` + font-size: 14px; + line-height: 22px; + ` + : ''}; +` + +export const ScApplyButton = styled(ApplyButton)` + width: 300px; + height: 60px; + margin: 0; + + ${isMobileDevice + ? css` + font-size: 14px; + margin: 20px 0; + width: 293px; + height: 50px; + + @media screen and (orientation: landscape) { + margin: 0; + width: 225px; + height: 44px; + } + ` + : ''}; + ${client.styles.popupApplyButton} +` + +export const Header = styled(BaseHeader)` + display: flex; + flex-direction: column; + align-items: center; + height: auto; + justify-content: center; + gap: 30px; + ${isMobileDevice + ? css` + @media screen and (orientation: landscape) { + gap: 10px; + + svg { + width: 40px; + height: 40px; + } + } + ` + : ''}; +` + +export const Footer = styled.div` + width: 100%; + display: flex; + justify-content: center; + padding: 33px 0 65px 0; + + ${isMobileDevice + ? css` + padding-top: 30px; + ` + : ''}; +` diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index 5d6e1344..9e2f6955 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -9,11 +9,16 @@ const matchPopupLexics = { apply: 13491, choose_fav_team: 19776, commentators: 15424, + continue_watching: 20007, + current_stats: 19592, + display_all_stats: 19932, + display_stats_according_to_video: 19931, episode_duration: 13410, events: 1020, from_end_match: 15396, from_price: 3992, from_start_match: 15395, + game_preview: 20005, gk: 3515, go_back_to_match: 13405, group: 7850, @@ -27,9 +32,12 @@ const matchPopupLexics = { playlist_format_all_actions: 13408, playlist_format_all_match_time: 13407, playlist_format_selected_acions: 13409, + sec_60: 20006, sec_after: 13412, sec_before: 13411, selected_player_actions: 13413, + sign_in: 20003, + sign_in_full_game: 20004, started_streaming_at: 16042, streamed_live_on: 16043, video: 1017, diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx index b006839c..834ef537 100644 --- a/src/config/procedures.tsx +++ b/src/config/procedures.tsx @@ -26,6 +26,7 @@ export const PROCEDURES = { get_user_preferences: 'get_user_preferences', get_user_subscribes: 'get_user_subscribes', get_user_subscriptions: 'get_user_subscriptions', + get_view_user_match: 'get_view_user_match', landing_get_match_info: 'landing_get_match_info', lst_c_country: 'lst_c_country', ott_match_events: 'ott_match_events', diff --git a/src/features/App/index.tsx b/src/features/App/index.tsx index 80f86180..3010ffcf 100644 --- a/src/features/App/index.tsx +++ b/src/features/App/index.tsx @@ -1,4 +1,8 @@ -import { Suspense } from 'react' +import { + Suspense, + useEffect, + useState, +} from 'react' import { Router } from 'react-router-dom' import { MatomoProvider } from '@jonkoops/matomo-tracker-react' @@ -8,40 +12,32 @@ import { client } from 'config/clients' import { matomoInstance } from 'config/matomo' import { isAvailable } from 'config/env' +import { readToken } from 'helpers' + import { setClientTitleAndDescription } from 'helpers/setClientHeads' -import { isMatchPage, isMatchPageRFEF } from 'helpers/isMatchPage' import { GlobalStores } from 'features/GlobalStores' -import { useAuthStore } from 'features/AuthStore' import { Background } from 'features/Background' import { GlobalStyles } from 'features/GlobalStyles' import { Theme } from 'features/Theme' -import { JoinMatchPage } from 'features/JoinMatchPage' -import { JoinMatchPageRFEF } from 'features/JoinMatchPageRFEF' import { UnavailableText } from 'components/UnavailableText' import { AuthenticatedApp } from './AuthenticatedApp' -import { checkPage } from '../../helpers/checkPage' -import { PAGES } from '../../config' +import { useAuthStore } from '../AuthStore' setClientTitleAndDescription(client.title, client.description) const Main = () => { - const { loadingUser, user } = useAuthStore() - - if (!user && (isMatchPage() || checkPage(PAGES.tournament))) return - if (!user && isMatchPageRFEF()) return + const [isToken, setIsToken] = useState(false) + const { userInfo } = useAuthStore() - if (user && isMatchPageRFEF()) { - window.location.href = 'https://instat.tv/football/tournaments/131' - } - // юзер считывается из localstorage или - // access_token токен истек и запрашивается новый - if (loadingUser || user?.expired) return null + useEffect(() => { + readToken() && setIsToken(true) + }, [userInfo]) // имеется действующий токен - return + return isToken ? : null } const date = new Date() @@ -57,8 +53,8 @@ const OTTApp = () => ( {isAvailable - && (date.getTime() < new Date(startDate).getTime() - || date.getTime() > new Date(stopDate).getTime()) + && (date.getTime() < new Date(startDate).getTime() + || date.getTime() > new Date(stopDate).getTime()) ? (
) : ()} diff --git a/src/features/AuthServiceApp/components/Login/hooks.tsx b/src/features/AuthServiceApp/components/Login/hooks.tsx index 04c3b4fb..5488400f 100644 --- a/src/features/AuthServiceApp/components/Login/hooks.tsx +++ b/src/features/AuthServiceApp/components/Login/hooks.tsx @@ -16,6 +16,8 @@ import { loginCheck } from 'features/AuthServiceApp/requests/auth' import { getApiUrl } from 'features/AuthServiceApp/config/routes' import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields' +import { addAccessTokenToUrl } from 'helpers/languageUrlParam' + import { AuthProviders } from '../../config/authProviders' import { getAuthUrl } from '../../helpers/getAuthUrl' import { useParamsUrl } from '../../hooks/useParamsUrl' @@ -38,7 +40,6 @@ export const useLoginForm = () => { const [isRecoveryPopupOpen, setIsRecoveryPopupOpen] = useState(false) const formRef = useRef(null) - const { email, error: formError, @@ -134,6 +135,6 @@ export const useLoginForm = () => { response_type, scope, setIsRecoveryPopupOpen, - url, + url: addAccessTokenToUrl(url), } } diff --git a/src/features/AuthServiceApp/components/Oauth/hooks.tsx b/src/features/AuthServiceApp/components/Oauth/hooks.tsx index c5bbfdd0..0d1f0801 100644 --- a/src/features/AuthServiceApp/components/Oauth/hooks.tsx +++ b/src/features/AuthServiceApp/components/Oauth/hooks.tsx @@ -9,8 +9,9 @@ import { } from 'react' import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail' +import { API_ROOT } from 'features/AuthServiceApp/config/routes' -import { API_ROOT } from '../../config/routes' +import { addAccessTokenToUrl } from 'helpers/languageUrlParam' export const useOauth = () => { const [email, setEmail] = useState('') @@ -24,7 +25,7 @@ export const useOauth = () => { const authorize = useCallback(async () => { if (!formRef.current) return - const url = `${API_ROOT}/oauth` + const url = addAccessTokenToUrl(`${API_ROOT}/oauth`) const res = await fetch(url, { body: new FormData(formRef.current), diff --git a/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx b/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx index a661f487..72782aed 100644 --- a/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx +++ b/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx @@ -6,11 +6,10 @@ import { devices } from 'config/devices' import { ModalWindow } from 'features/Modal/styled' import { Modal as BaseModal } from 'features/Modal' import { Header as BaseHeader } from 'features/PopupComponents' +import { client } from 'features/AuthServiceApp/config/clients' import { ButtonSolid } from 'features/Common' -import { client } from '../../config/clients' - export const Modal = styled(BaseModal)` background-color: rgba(0, 0, 0, 0.7); padding: 0 60px; @@ -52,9 +51,12 @@ export const Wrapper = styled.div` ` export const Header = styled(BaseHeader)` + display: flex; + flex-direction: column; + align-items: center; height: auto; - padding-top: 60; justify-content: center; + gap: 30px; ${isMobileDevice ? css` @media ${devices.mobile}{ diff --git a/src/features/AuthServiceApp/requests/register.tsx b/src/features/AuthServiceApp/requests/register.tsx index 7573b348..25c4068e 100644 --- a/src/features/AuthServiceApp/requests/register.tsx +++ b/src/features/AuthServiceApp/requests/register.tsx @@ -1,5 +1,7 @@ import type { ClientIds } from 'config/clients/types' +import { checkCookie } from 'helpers/cookie' + import { getApiUrl } from 'features/AuthServiceApp/config/routes' const errorLexics = { @@ -45,8 +47,10 @@ export const registerCheck = async ({ password, urlParams, } : RegisterProps) => { - const url = getApiUrl('/registration') - + const url = ` + ${getApiUrl('/registration')}${checkCookie('access_token') + ? `&${checkCookie('access_token')}` + : ''}` const init: RequestInit = { body: new URLSearchParams({ email, @@ -56,7 +60,6 @@ export const registerCheck = async ({ method: 'POST', } const response = await fetch(url, init) - const body: SuccessResponse | FailedResponse = await response.json() if (body.ok) return Promise.resolve() diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx index fa414650..5e060e34 100644 --- a/src/features/AuthStore/hooks/useAuth.tsx +++ b/src/features/AuthStore/hooks/useAuth.tsx @@ -17,9 +17,16 @@ import { PAGES } from 'config' import { addLanguageUrlParam, } from 'helpers/languageUrlParam' -import { writeToken, removeToken } from 'helpers/token' -import { setCookie, removeCookie } from 'helpers/cookie' -import { isMatchPage, isMatchPageRFEF } from 'helpers/isMatchPage' + +import { + writeToken, + removeToken, + readToken, +} from 'helpers/token' +import { + setCookie, + removeCookie, +} from 'helpers/cookie' import { useLocalStore, useToggle } from 'hooks' @@ -30,7 +37,7 @@ import { getUserInfo, UserInfo } from 'requests/getUserInfo' import { checkDevice, FailedResponse } from 'requests/checkDevice' import { getClientSettings, needCheckNewDeviсe } from '../helpers' -import { checkPage } from '../../../helpers/checkPage' +import { getTokenVirtualUser } from '../../../requests' export const useAuth = () => { const { changeLang, lang } = useLexicsStore() @@ -44,18 +51,25 @@ export const useAuth = () => { const [userInfo, setUserInfo] = useState() const userManager = useMemo(() => new UserManager(getClientSettings()), []) - const login = useCallback(async () => ( + const login = useCallback(async () => { userManager.signinRedirect({ extraQueryParams: { lang } }) - ), [userManager, lang]) + }, [userManager, lang]) - const logout = useCallback(() => { + const logout = useCallback((key?: string) => { + if (history.location.pathname === '/') { + setSearch(history.location.search) + } + setPage(history.location.pathname) userManager.clearStaleState() userManager.createSigninRequest().then(({ url }) => { const urlWithLang = addLanguageUrlParam(lang, url) userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang }) }) - removeToken() - removeCookie('access_token') + if (key !== 'saveToken') { + removeToken() + removeCookie('access_token') + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [userManager, lang]) const storeUser = useCallback((loadedUser: User) => { @@ -70,11 +84,19 @@ export const useAuth = () => { const checkUser = useCallback(async () => { const loadedUser = await userManager.getUser() - if (!loadedUser) return Promise.reject() + + if (!loadedUser) { + if (!readToken()) { + const token = await getTemporaryToken() + token && await fetchUserInfo() + } + return Promise.resolve() + } storeUser(loadedUser) markUserLoaded() return loadedUser + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ userManager, storeUser, @@ -95,6 +117,23 @@ export const useAuth = () => { validator: isString, }) + const getTemporaryToken = async () => { + try { + const { access_token } = await getTokenVirtualUser() + + writeToken(access_token) + setCookie({ + exdays: 1, + name: 'access_token', + value: access_token, + }) + return access_token + // eslint-disable-next-line no-empty + } catch { + return '' + } + } + const signinRedirectCallback = useCallback(() => { userManager.signinRedirectCallback() .then((loadedUser) => { @@ -104,10 +143,7 @@ export const useAuth = () => { markUserLoaded() if (page) { const route = `${page}${page === '/' ? search : ''}` - history.push(`${route}${( - page.includes('tournaments') || page.includes('matches')) - ? '?from=landing' - : ''}`) + history.push(route) setPage('') setSearch('') } @@ -129,11 +165,7 @@ export const useAuth = () => { if (isRedirectedBackFromAuthProvider) { signinRedirectCallback() } else { - checkUser().catch(() => { - if (!isMatchPage() && !isMatchPageRFEF() && !checkPage(PAGES.tournament)) { - login() - } - + checkUser().catch(async () => { if (history.location.pathname === '/') { setSearch(history.location.search) } @@ -212,14 +244,12 @@ export const useAuth = () => { userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) - // eslint-disable-next-line no-empty + // eslint-disable-next-line no-empty } catch (error) {} }, [changeLang]) useEffect(() => { - if (user) { - fetchUserInfo() - } + fetchUserInfo() }, [fetchUserInfo, user]) const auth = useMemo(() => ({ @@ -228,6 +258,7 @@ export const useAuth = () => { loadingUser, login, logout, + setUserInfo, user, userInfo, }), [ @@ -238,6 +269,7 @@ export const useAuth = () => { userInfo, login, loadingUser, + setUserInfo, ]) return auth diff --git a/src/features/FavoritesMobilePopup/components/GroupBlock/index.tsx b/src/features/FavoritesMobilePopup/components/GroupBlock/index.tsx index 953fa505..94e08c0d 100644 --- a/src/features/FavoritesMobilePopup/components/GroupBlock/index.tsx +++ b/src/features/FavoritesMobilePopup/components/GroupBlock/index.tsx @@ -69,7 +69,7 @@ export const GroupBlock = ({ groupBlock }: Props) => { {name} - + {countryOrTeam} diff --git a/src/features/HomePage/hooks.tsx b/src/features/HomePage/hooks.tsx index 83cc160b..4fb0f3c7 100644 --- a/src/features/HomePage/hooks.tsx +++ b/src/features/HomePage/hooks.tsx @@ -34,26 +34,29 @@ const getTimezoneOffset = (date: Date) => { const getDate = (date: Date) => format(date, 'yyyy-MM-dd') export const useHomePage = () => { - const { user, userInfo } = useAuthStore() + const { userInfo } = useAuthStore() const { selectedDate } = useHeaderFiltersStore() const [isOpenDownload, setIsOpenDownload] = useState(false) const [isShowConfirmPopup, setIsShowConfirmPopup] = useState(false) const setIsSportFilterShown = useSetRecoilState(isSportFilterShownAtom) const handleCloseConfirmPopup = useCallback(async () => { - await setAgreements(user?.profile?.email || '') + await setAgreements(`${userInfo?.email}` || '') setIsShowConfirmPopup(false) - }, [setIsShowConfirmPopup, user]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setIsShowConfirmPopup, userInfo]) useEffect(() => { - (async () => { - const agreement = await getAgreements(user?.profile?.email || '') - if (!agreement?.is_agreement_privacy_policy && !agreement?.is_agreement_terms_conditions) { - setIsShowConfirmPopup(true) - } - })() + if (userInfo?.email) { + (async () => { + const agreement = await getAgreements(`${userInfo?.email}` || '') + if (!agreement?.is_agreement_privacy_policy && !agreement?.is_agreement_terms_conditions) { + setIsShowConfirmPopup(true) + } + })() + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [userInfo]) useEffect(() => { const dateLastOpenSmartBanner = localStorage.getItem('dateLastOpenSmartBanner') @@ -71,7 +74,8 @@ export const useHomePage = () => { offset, timezoneOffset: getTimezoneOffset(selectedDate), }), - [selectedDate], + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedDate, userInfo], ) useEffect(() => { diff --git a/src/features/ItemsList/index.tsx b/src/features/ItemsList/index.tsx index 5788b6d3..1f9534c7 100644 --- a/src/features/ItemsList/index.tsx +++ b/src/features/ItemsList/index.tsx @@ -55,7 +55,7 @@ export const ItemsList = ({ {item.additionalInfo && ( - + )} diff --git a/src/features/MatchCard/hooks.tsx b/src/features/MatchCard/hooks.tsx index 4187cdfa..2083e3af 100644 --- a/src/features/MatchCard/hooks.tsx +++ b/src/features/MatchCard/hooks.tsx @@ -34,6 +34,9 @@ export const useCard = (match: Match) => { const onMatchClick = useCallback(() => { switch (match.access) { + case MatchAccess.ViewMatchPopupWithoutUser: + redirectToMatchPage() + break case MatchAccess.CanBuyMatch: openBuyMatchPopup(match) break diff --git a/src/features/MatchPage/components/FinishedMatch/index.tsx b/src/features/MatchPage/components/FinishedMatch/index.tsx index d10f60d0..decd92e5 100644 --- a/src/features/MatchPage/components/FinishedMatch/index.tsx +++ b/src/features/MatchPage/components/FinishedMatch/index.tsx @@ -17,7 +17,11 @@ import { useMatchPageStore } from '../../store' export const FinishedMatch = () => { const [circleAnimation, setCircleAnimation] = useState(initialCircleAnimation) - const { isOpenPopup, profile } = useMatchPageStore() + const { + access, + isOpenFiltersPopup, + profile, + } = useMatchPageStore() const { chapters, closeSettingsPopup, @@ -47,8 +51,9 @@ export const FinishedMatch = () => { {!isEmpty(chapters) && ( { const { profile: matchProfile } = useMatchPageStore() const { open: openBuyMatchPopup } = useBuyMatchPopupStore() const { profileId: matchId, sportType } = usePageParams() + const { user } = useAuthStore() useEffect(() => { - if (matchProfile && ( + if (user && matchProfile && ( !matchProfile.sub || (checkUrlParams('subscribe') && getAllUrlParams('id')))) { @@ -35,11 +37,12 @@ export const SubscriptionGuard = ({ children }: Props) => { openBuyMatchPopup, matchProfile, sportType, + user, ]) return ( - {matchProfile?.sub ? children : null} + {matchProfile?.sub || !user ? children : null} ) } diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 112a6a4d..5e35b73f 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -31,8 +31,12 @@ const MatchPageComponent = () => { const history = useHistory() const { addRemoveFavorite, userFavorites } = useUserFavoritesStore() - const { isStarted, profile } = useMatchPageStore() - const isFavorite = profile && userFavorites.find((fav) => fav.id === profile?.tournament.id) + const { + isStarted, + profile, + user, + } = useMatchPageStore() + const isFavorite = profile && userFavorites?.find((fav) => fav.id === profile?.tournament.id) const { profileType, @@ -100,7 +104,7 @@ const MatchPageComponent = () => {
{ - (profile?.tournament.id === 131 || profile?.tournament.id === 2032) + user && (profile?.tournament.id === 131 || profile?.tournament.id === 2032) && } diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 184f47e9..852e0566 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -9,9 +9,11 @@ import filter from 'lodash/filter' import isEmpty from 'lodash/isEmpty' import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' +import { useAuthStore } from 'features/AuthStore' import type { MatchInfo } from 'requests/getMatchInfo' import { getMatchInfo } from 'requests/getMatchInfo' +import { getViewMatchDuration } from 'requests/getViewMatchDuration' import { usePageParams } from 'hooks/usePageParams' import { useToggle } from 'hooks/useToggle' @@ -23,32 +25,68 @@ import { useMatchData } from './useMatchData' import { useFiltersPopup } from './useFitersPopup' import { useTabEvents } from './useTabEvents' +const ACCESS_TIME = 60 + export const useMatchPage = () => { const [matchProfile, setMatchProfile] = useState(null) const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false) + const [access, setAccess] = useState(true) const { profileId: matchId, sportType } = usePageParams() + + const { user, userInfo } = useAuthStore() + const { close: hideProfileCard, isOpen: profileCardShown, open: showProfileCard, } = useToggle(true) + const { + events, + handlePlaylistClick, + matchPlaylists, + selectedPlaylist, + setFullMatchPlaylistDuration, + } = useMatchData(matchProfile) + + const profile = matchProfile + const { activeEvents, activeFirstTeamPlayers, activeSecondTeamPlayers, + allActionsToggle, + allPlayersToggle, applyFilters, close: closePopup, countOfFilters, filters, + isAllActionsChecked, isEmptyFilters, - isOpen: isOpenPopup, - resetEvents, - resetPlayers, + isFirstTeamPlayersChecked, + isOpen: isOpenFiltersPopup, + isSecondTeamPlayersChecked, toggle: togglePopup, toggleActiveEvents, toggleActivePlayers, - } = useFiltersPopup() + uniqEvents, + } = useFiltersPopup({ + events, + matchPlaylists, + }) + + const getMatchViewDuration = (id: number) => (getViewMatchDuration({ + matchId, + sportType, + userId: id, + }).then(({ + duration, + error, + }) => { + if (error || (duration && Number(duration) > ACCESS_TIME)) { + setAccess(false) + } + })) useEffect(() => { getMatchInfo(sportType, matchId).then(setMatchProfile) @@ -62,7 +100,25 @@ export const useMatchPage = () => { ) } return () => clearInterval(getIntervalMatch) - }, [matchProfile, sportType, matchId]) + }, [ + matchProfile, + sportType, + matchId]) + + useEffect(() => { + if (user || !userInfo?.email) return + + const counter = setInterval( + () => getMatchViewDuration(Number(userInfo?.email)), 1000 * 30, + ) + // eslint-disable-next-line + return () => clearInterval(counter) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + sportType, + matchId, + userInfo, + ]) useEffect(() => { let getIntervalMatch: ReturnType @@ -75,16 +131,6 @@ export const useMatchPage = () => { return () => clearInterval(getIntervalMatch) }) - const { - events, - handlePlaylistClick, - matchPlaylists, - selectedPlaylist, - setFullMatchPlaylistDuration, - } = useMatchData(matchProfile) - - const profile = matchProfile - const isStarted = useMemo(() => ( profile?.date ? parseDate(profile.date) < new Date() @@ -159,10 +205,13 @@ export const useMatchPage = () => { } return { + access, activeEvents, activeFirstTeamPlayers, activeSecondTeamPlayers, activeStatus, + allActionsToggle, + allPlayersToggle, applyFilters, closePopup, countOfFilters, @@ -171,10 +220,13 @@ export const useMatchPage = () => { filteredEvents, handlePlaylistClick, hideProfileCard, + isAllActionsChecked, isEmptyFilters, + isFirstTeamPlayersChecked, isLiveMatch, - isOpenPopup, + isOpenFiltersPopup, isPlayFilterEpisodes, + isSecondTeamPlayersChecked, isStarted, likeImage, likeToggle, @@ -184,8 +236,6 @@ export const useMatchPage = () => { playNextEpisode, profile, profileCardShown, - resetEvents, - resetPlayers, reversedGroupEvents, selectedPlaylist, setFullMatchPlaylistDuration, @@ -199,6 +249,8 @@ export const useMatchPage = () => { toggleActivePlayers, togglePopup, tournamentData, + uniqEvents, + user, watchAllEpisodesTimer, } } diff --git a/src/features/MatchPage/store/hooks/useFitersPopup.tsx b/src/features/MatchPage/store/hooks/useFitersPopup.tsx index 78fa4e06..37b096ab 100644 --- a/src/features/MatchPage/store/hooks/useFitersPopup.tsx +++ b/src/features/MatchPage/store/hooks/useFitersPopup.tsx @@ -4,6 +4,13 @@ import includes from 'lodash/includes' import filter from 'lodash/filter' import isEmpty from 'lodash/isEmpty' import size from 'lodash/size' +import uniq from 'lodash/uniq' +import difference from 'lodash/difference' +import map from 'lodash/map' +import every from 'lodash/every' + +import type { Events } from 'requests' +import type { Playlists } from 'features/MatchPage/types' type TTogglePlayers = { id: Number, @@ -15,13 +22,36 @@ type TFilters = { players: Array, } -export const useFiltersPopup = () => { +type Props = { + events: Events, + matchPlaylists: Playlists, +} + +export const useFiltersPopup = ({ + events, + matchPlaylists, +}: Props) => { const [isOpen, setIsOpen] = useState(false) const [activeEvents, setActiveEvents] = useState>([]) const [activeFirstTeamPlayers, setActiveFirstTeamPlayers] = useState>([]) const [activeSecondTeamPlayers, setActiveSecondTeamPlayers] = useState>([]) const [activeFilters, setActiveFilters] = useState({ events: [], players: [] }) + const currentEvents = filter(events, (event) => event.pl !== undefined + && event.t !== undefined) + + const uniqEvents = uniq(map(currentEvents, ({ l }) => l)) + const uniqPlayersTeam1 = uniq(map(matchPlaylists.players.team1, ({ id }) => id)) + const uniqPlayersTeam2 = uniq(map(matchPlaylists.players.team2, ({ id }) => id)) + + const isAllActionsChecked = every(uniqEvents, (el) => (includes(activeEvents, el))) + const isFirstTeamPlayersChecked = every( + uniqPlayersTeam1, (el) => (includes(activeFirstTeamPlayers, el)), + ) + const isSecondTeamPlayersChecked = every( + uniqPlayersTeam2, (el) => (includes(activeSecondTeamPlayers, el)), + ) + const toggle = () => { setIsOpen(!isOpen) } @@ -50,22 +80,28 @@ export const useFiltersPopup = () => { : [...teamState, id]) } - const resetPlayers = (team: string) => () => { - const teamState = team === 'team1' - ? activeFirstTeamPlayers - : activeSecondTeamPlayers - + const allPlayersToggle = (team: string) => () => { const setterTeamState = team === 'team1' ? setActiveFirstTeamPlayers : setActiveSecondTeamPlayers - if (isEmpty(teamState)) return - setterTeamState([]) + const uniqValues = team === 'team1' + ? uniqPlayersTeam1 + : uniqPlayersTeam2 + + const isAllPlayersChecked = team === 'team1' + ? isFirstTeamPlayersChecked + : isSecondTeamPlayersChecked + + isAllPlayersChecked + ? setterTeamState((currentValues) => difference(currentValues, uniqValues)) + : setterTeamState((currentValues) => uniq([...currentValues, ...uniqValues])) } - const resetEvents = () => { - if (isEmpty(activeEvents)) return - setActiveEvents([]) + const allActionsToggle = () => { + isAllActionsChecked + ? setActiveEvents((currentValues) => difference(currentValues, uniqEvents)) + : setActiveEvents((currentValues) => uniq([...currentValues, ...uniqEvents])) } const applyFilters = () => { @@ -86,16 +122,20 @@ export const useFiltersPopup = () => { activeEvents, activeFirstTeamPlayers, activeSecondTeamPlayers, + allActionsToggle, + allPlayersToggle, applyFilters, close, countOfFilters, filters: activeFilters, + isAllActionsChecked, isEmptyFilters, + isFirstTeamPlayersChecked, isOpen, - resetEvents, - resetPlayers, + isSecondTeamPlayersChecked, toggle, toggleActiveEvents, toggleActivePlayers, + uniqEvents, } } diff --git a/src/features/MatchSidePlaylists/components/FiltersPopup/index.tsx b/src/features/MatchSidePlaylists/components/FiltersPopup/index.tsx index e5106500..4cac7015 100644 --- a/src/features/MatchSidePlaylists/components/FiltersPopup/index.tsx +++ b/src/features/MatchSidePlaylists/components/FiltersPopup/index.tsx @@ -1,8 +1,7 @@ -import uniq from 'lodash/uniq' import map from 'lodash/map' -import isEmpty from 'lodash/isEmpty' import includes from 'lodash/includes' -import filter from 'lodash/filter' + +import { isMobileDevice } from 'config/userAgent' import { T9n } from 'features/T9n' import { useMatchPageStore } from 'features/MatchPage/store' @@ -28,14 +27,18 @@ import { } from './styled' type TLabelProps = { + checked: boolean, player: PlayerPlaylistOption, } -const Label = ({ player }: TLabelProps) => { +const Label = ({ + checked, + player, +}: TLabelProps) => { const { num } = player return ( - + {num} @@ -48,20 +51,19 @@ export const FiltersPopup = () => { activeEvents, activeFirstTeamPlayers, activeSecondTeamPlayers, + allActionsToggle, + allPlayersToggle, applyFilters, closePopup, - events, + isAllActionsChecked, + isFirstTeamPlayersChecked, + isSecondTeamPlayersChecked, matchPlaylists, profile, - resetEvents, - resetPlayers, toggleActiveEvents, toggleActivePlayers, + uniqEvents, } = useMatchPageStore() - const currentEvents = filter(events, (event) => event.pl !== undefined - && event.t !== undefined) - - const uniqEvents = uniq(map(currentEvents, ({ l }) => l)) const team1Name = useName(profile!.team1) const team2Name = useName(profile!.team2) @@ -70,7 +72,10 @@ export const FiltersPopup = () => { - + @@ -80,8 +85,9 @@ export const FiltersPopup = () => { @@ -100,41 +106,49 @@ export const FiltersPopup = () => { - {map(matchPlaylists.players.team1, ((player) => ( - - )} - /> - - )))} + {map(matchPlaylists.players.team1, ((player) => { + const isCheckboxChecked = includes(activeFirstTeamPlayers, player.id) + return ( + + )} + /> + + ) + }))} - {map(matchPlaylists.players.team2, ((player) => ( - - )} - /> - - )))} + {map(matchPlaylists.players.team2, ((player) => { + const isCheckboxChecked = includes(activeSecondTeamPlayers, player.id) + return ( + + )} + /> + + ) + }))} diff --git a/src/features/MatchSidePlaylists/components/FiltersPopup/styled.tsx b/src/features/MatchSidePlaylists/components/FiltersPopup/styled.tsx index 0c96635d..3bcc1709 100644 --- a/src/features/MatchSidePlaylists/components/FiltersPopup/styled.tsx +++ b/src/features/MatchSidePlaylists/components/FiltersPopup/styled.tsx @@ -8,6 +8,15 @@ import { NameStyled } from 'features/Name' import { isMobileDevice } from 'config/userAgent' import { BaseButton } from 'features/PopupComponents' +type CheckboxProps = { + checked: boolean, + isMainCheckbox?: boolean, +} + +type PlayerNumberProps = { + checked: boolean, +} + export const PopupContainer = styled.div` background: #333333; box-shadow: 0px 2px 40px rgba(0, 0, 0, 0.6); @@ -35,6 +44,8 @@ export const PopupInner = styled.div` max-height: 100%; height: 100%; overflow-y: auto; + padding-bottom: ${(isMobileDevice ? '20%' : '')}; + ${customScrollbar} ` @@ -81,8 +92,16 @@ export const HeaderText = styled.div` export const PartBlock = styled.div` flex: 1 0 auto; - border-bottom: 1px solid #505050; padding: 17px 22px 17px 22px; + + ${isMobileDevice + ? css` + border-bottom: 1px solid #505050;` + : css` + :not(:last-child) { + border-bottom: 1px solid #505050; + } + `}; ` export const ItemsContainer = styled.ul` @@ -92,7 +111,7 @@ export const ItemsContainer = styled.ul` ` export const MainCheckboxContainer = styled.div` - margin-bottom: 15px; + margin-bottom: ${(isMobileDevice ? '10px' : '15px')}; display: flex; ` @@ -107,14 +126,13 @@ export const ItemBlock = styled.div` flex: 0 0 33.33%;`} ` -export const Checkbox = styled(BaseCheckbox)` +export const Checkbox = styled(BaseCheckbox)` height: 28px; display: block; ${Label} { height: 100%; font-style: normal; - font-weight: 400; ${isMobileDevice ? css` @@ -123,18 +141,28 @@ export const Checkbox = styled(BaseCheckbox)` : css` font-size: 14px; line-height: 16px;`} - - ${({ checked }) => (checked - ? css`` + + ${({ checked, isMainCheckbox }) => (isMainCheckbox + ? css` + font-weight: 500;` : css` - color: rgba(255, 255, 255, 0.6);` + color: ${checked ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.3)'}; + font-weight: 400;` )} } ${CheckboxSvg} { + ${({ checked, isMainCheckbox }) => (isMainCheckbox + ? css` + width: ${(isMobileDevice ? '23px' : '20px')}; + height: ${(isMobileDevice ? '23px' : '20px')};` + : css` + fill: ${checked ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.3)'}; + width: ${(isMobileDevice ? '18px' : '14px')}; + height: ${(isMobileDevice ? '18px' : '14px')}; + margin-left: ${(isMobileDevice ? '2px' : '3px')};` + )} margin-right: 8px; - width: 20px; - height: 20px; } ` @@ -157,8 +185,6 @@ export const ItemText = styled.div` width: 130px;`} ` -export const TeamBlock = styled.div`` - export const ButtonConatiner = styled.div` display: flex; align-items: center; @@ -171,7 +197,8 @@ export const ButtonConatiner = styled.div` bottom: 14px; left: 50%; transform: translate(-50%, 0);` - : css``} + : css` + border-top: 1px solid #505050;`} ` export const Button = styled.button` @@ -191,12 +218,12 @@ export const Button = styled.button` ${isMobileDevice ? css` - width: 301px;` + width: 190px;` : css` width: 167px;`} ` -export const PlayerNumber = styled.span` - color: rgba(255, 255, 255, 0.7); +export const PlayerNumber = styled.span` + color: ${({ checked }) => (checked ? '#fff' : 'rgba(255, 255, 255, 0.3)')}; margin-right: 5px; min-width: 14px; font-size: 11px; diff --git a/src/features/Matches/helpers/getMatchClickAction/index.tsx b/src/features/Matches/helpers/getMatchClickAction/index.tsx index 12f9e9b6..4539f329 100644 --- a/src/features/Matches/helpers/getMatchClickAction/index.tsx +++ b/src/features/Matches/helpers/getMatchClickAction/index.tsx @@ -1,26 +1,29 @@ import type { Match } from 'requests' +import type { User } from 'oidc-client' + export enum MatchAccess { CanBuyMatch = 'CanBuyMatch', NoAccess = 'NoAccess', NoCountryAccess = 'NoCountryAccess', RedirectToProfile = 'RedirectToProfile', ViewMatchPopup = 'ViewMatchPopup', + ViewMatchPopupWithoutUser = 'ViewMatchPopupWithoutUser', } -export const getMatchAccess = ({ - access, - calc, - date, - has_video, - live, - storage, - sub, -}: Match) => { +export const getMatchAccess = (match: Match, user: User | undefined) => { + const { + access, + date, + live, + sub, + } = match const dateToMs = Date.parse(date?.replace(/ /, 'T')) // без замены не будет работать в сафари const dateNowMin10 = dateToMs - 10 * 60 * 1000 switch (true) { + case !user: + return MatchAccess.ViewMatchPopupWithoutUser case !sub: return MatchAccess.CanBuyMatch case !access: diff --git a/src/features/Matches/helpers/prepareMatches.tsx b/src/features/Matches/helpers/prepareMatches.tsx index f218cd87..bec2581b 100644 --- a/src/features/Matches/helpers/prepareMatches.tsx +++ b/src/features/Matches/helpers/prepareMatches.tsx @@ -2,13 +2,15 @@ import map from 'lodash/map' import orderBy from 'lodash/orderBy' import format from 'date-fns/format' +import type { User } from 'oidc-client' + import type { Match } from 'requests' import { parseDate } from 'helpers/parseDate' import { getMatchAccess } from './getMatchClickAction' -const prepareMatch = (match: Match) => { +const prepareMatch = (match: Match, user?: User | undefined) => { const { calc, country, @@ -31,7 +33,7 @@ const prepareMatch = (match: Match) => { const date = parseDate(matchDate) return { - access: getMatchAccess(match), + access: getMatchAccess(match, user), calc, countryId: country_id, countryInfo: country, @@ -54,10 +56,10 @@ const prepareMatch = (match: Match) => { } } -export const prepareMatches = (matches: Array) => { +export const prepareMatches = (matches: Array, user?: User | undefined) => { const preparedMatches = map( matches, - prepareMatch, + (match) => prepareMatch(match, user), ) return orderBy( preparedMatches, diff --git a/src/features/Matches/hooks.tsx b/src/features/Matches/hooks.tsx index 2235bb4e..ec59b014 100644 --- a/src/features/Matches/hooks.tsx +++ b/src/features/Matches/hooks.tsx @@ -15,6 +15,8 @@ import { isMobileDevice } from 'config/userAgent' import { prepareMatches } from './helpers/prepareMatches' +import { useAuthStore } from '../AuthStore' + export type Match = ReturnType[number] export type Props = { @@ -33,6 +35,7 @@ const initialState = { export const useMatches = ({ fetch }: Props) => { const { userPreferences } = usePreferencesStore() + const { user } = useAuthStore() const { isFetching, request: requestMatches, @@ -81,10 +84,11 @@ export const useMatches = ({ fetch }: Props) => { }, [fetchMatches, userPreferences]) const preparedMatches = useMemo(() => ({ - broadcast: prepareMatches(matches.broadcast), - features: prepareMatches(matches.features), - highlights: prepareMatches(matches.highlights), + broadcast: prepareMatches(matches.broadcast, user), + features: prepareMatches(matches.features, user), + highlights: prepareMatches(matches.highlights, user), isVideoSections: matches.isVideoSections, + // eslint-disable-next-line react-hooks/exhaustive-deps }), [matches]) return { diff --git a/src/features/Menu/index.tsx b/src/features/Menu/index.tsx index 3cba50bb..e6413b04 100644 --- a/src/features/Menu/index.tsx +++ b/src/features/Menu/index.tsx @@ -6,6 +6,8 @@ import { PAGES } from 'config/pages' import { usePreferencesStore } from 'features/PreferencesPopup' import { useTournamentPopupStore } from 'features/TournamentsPopup/store' +import { useAuthStore } from 'features/AuthStore' + import { FavoritesMobilePopup } from '../FavoritesMobilePopup' import { @@ -18,6 +20,7 @@ export const Menu = () => { const { openPopup } = usePreferencesStore() const { open } = useTournamentPopupStore() const isHomePage = useRouteMatch(PAGES.home)?.isExact + const { logout, user } = useAuthStore() return ( @@ -29,8 +32,14 @@ export const Menu = () => { ) } - - + { + e.preventDefault() + !user && logout('saveToken') + }} + > + diff --git a/src/features/Modal/styled.tsx b/src/features/Modal/styled.tsx index 57a441ef..b6f32776 100644 --- a/src/features/Modal/styled.tsx +++ b/src/features/Modal/styled.tsx @@ -11,7 +11,7 @@ export const ModalContainer = styled.div` display: flex; align-items: center; justify-content: center; - z-index: 15; + z-index: 51; color: white; font-weight: 600; ` diff --git a/src/features/MultiSourcePlayer/hooks/index.tsx b/src/features/MultiSourcePlayer/hooks/index.tsx index 22022292..37097c1d 100644 --- a/src/features/MultiSourcePlayer/hooks/index.tsx +++ b/src/features/MultiSourcePlayer/hooks/index.tsx @@ -53,6 +53,7 @@ const initialState = { export type PlayerState = typeof initialState export type Props = { + access: boolean, chapters: Chapters, isOpenPopup?: boolean, onError?: () => void, @@ -214,6 +215,10 @@ export const useMultiSourcePlayer = ({ onPlayingChange(playing) }, [playing, onPlayingChange]) + useEffect(() => { + setPlayerState({ ...initialState }) + }, [profileId, setPlayerState]) + useEffect(() => { setCircleAnimation((state) => ({ ...state, @@ -286,11 +291,13 @@ export const useMultiSourcePlayer = ({ // ведем статистику просмотра матча const { start: startCollectingStats, stop: stopCollectingStats } = useInterval({ callback: () => { - saveMatchStats({ - matchId: profileId, - matchSecond: timeForStatistics.current, - sportType, - }) + if (timeForStatistics.current !== 0) { + saveMatchStats({ + matchId: profileId, + matchSecond: timeForStatistics.current, + sportType, + }) + } }, intervalDuration: VIEW_INTERVAL_MS, startImmediate: false, @@ -302,7 +309,12 @@ export const useMultiSourcePlayer = ({ } else { stopCollectingStats() } - }, [playing, startCollectingStats, stopCollectingStats]) + }, [ + playing, + startCollectingStats, + stopCollectingStats, + profileId, + ]) return { activeChapterIndex, diff --git a/src/features/MultiSourcePlayer/index.tsx b/src/features/MultiSourcePlayer/index.tsx index 5881a26c..f75e1ada 100644 --- a/src/features/MultiSourcePlayer/index.tsx +++ b/src/features/MultiSourcePlayer/index.tsx @@ -14,16 +14,24 @@ import { Controls } from 'features/StreamPlayer/components/Controls' import { Name } from 'features/Name' import RewindMobile from 'features/StreamPlayer/components/RewindMobile' import { FiltersPopup } from 'features/MatchSidePlaylists/components/FiltersPopup' +import { useAuthStore } from 'features/AuthStore' import { isMobileDevice } from 'config/userAgent' +import { AccessTimer } from 'components/AccessTimer' + import type { Props } from './hooks' import { useMultiSourcePlayer } from './hooks' import { Players } from './types' import { REWIND_SECONDS } from './config' export const MultiSourcePlayer = (props: Props) => { - const { isOpenPopup, profile } = props + const { + access, + isOpenPopup, + profile, + } = props + const { activeChapterIndex, activePlayer, @@ -78,6 +86,7 @@ export const MultiSourcePlayer = (props: Props) => { } = useMultiSourcePlayer(props) const firstPlayerActive = activePlayer === Players.PLAYER1 const currentVideo = firstPlayerActive ? video1Ref : video2Ref + const { user } = useAuthStore() return ( { volumeInPercent={volumeInPercent} /> + {!user && ( + + )} ) } diff --git a/src/features/PopupComponents/BaseButton/index.tsx b/src/features/PopupComponents/BaseButton/index.tsx index 1b2d73d9..76684a1b 100644 --- a/src/features/PopupComponents/BaseButton/index.tsx +++ b/src/features/PopupComponents/BaseButton/index.tsx @@ -31,8 +31,8 @@ export const BaseButton = styled.button` ${isMobileDevice ? css` - width: 18px; - height: 18px; + width: 20px; + height: 20px; padding: 4px; position: absolute; top: -20px; diff --git a/src/features/PreferencesPopup/components/TournamentInfo/index.tsx b/src/features/PreferencesPopup/components/TournamentInfo/index.tsx index b168cf29..ac4c53fd 100644 --- a/src/features/PreferencesPopup/components/TournamentInfo/index.tsx +++ b/src/features/PreferencesPopup/components/TournamentInfo/index.tsx @@ -51,7 +51,7 @@ export const TournamentInfo = ({ {isIcon && } - + diff --git a/src/features/ProfileCard/index.tsx b/src/features/ProfileCard/index.tsx index 43e66854..464b42d6 100644 --- a/src/features/ProfileCard/index.tsx +++ b/src/features/ProfileCard/index.tsx @@ -75,7 +75,7 @@ export const ProfileCard = ({ profile }: ProfileType) => {
{name} - + {tournamentId ? ( { - const { isOpenPopup, profile } = useMatchPageStore() + const { isOpenFiltersPopup, profile } = useMatchPageStore() const { onMouseMove, @@ -34,7 +34,7 @@ export const YoutubePlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {isOpenPopup && } + {isOpenFiltersPopup && } { - saveMatchStats({ - matchId: profileId, - matchSecond: timeForStatistics.current, - sportType, - }) + if (timeForStatistics.current !== 0) { + saveMatchStats({ + matchId: profileId, + matchSecond: timeForStatistics.current, + sportType, + }) + } }, intervalDuration: VIEW_INTERVAL_MS, startImmediate: false, @@ -462,7 +464,12 @@ export const useVideoPlayer = ({ } else { stopCollectingStats() } - }, [playing, startCollectingStats, stopCollectingStats]) + }, [ + playing, + startCollectingStats, + stopCollectingStats, + profileId, + ]) return { activeChapterIndex, diff --git a/src/features/StreamPlayer/hooks/useVideoQuality.tsx b/src/features/StreamPlayer/hooks/useVideoQuality.tsx index 25d1924e..dd1f97be 100644 --- a/src/features/StreamPlayer/hooks/useVideoQuality.tsx +++ b/src/features/StreamPlayer/hooks/useVideoQuality.tsx @@ -15,6 +15,7 @@ import isString from 'lodash/isString' import filter from 'lodash/fp/filter' import { useLocalStore } from 'hooks' +import { isMobileDevice } from 'config/userAgent' const autoQuality = { label: 'Auto', @@ -43,7 +44,7 @@ const getVideoQualities = (levels: Array) => { Number, 'desc', ) - return uniqBy([...sorted], 'label') + return uniqBy([...sorted, autoQuality], 'label') } export const useVideoQuality = (hls: Hls | null) => { @@ -73,11 +74,18 @@ export const useVideoQuality = (hls: Hls | null) => { const listener = () => { const qualities = getVideoQualities(hls.levels) - const quality = find(qualities, { label: selectedQuality }) || qualities[0] + const quality = find(qualities, { label: selectedQuality }) || autoQuality // eslint-disable-next-line no-param-reassign hls.currentLevel = quality.level setSelectedQuality(quality.label) setVideoQualities(qualities) + + if (isMobileDevice && quality.label === 'Auto') { + const mob720 = qualities.find((item) => item.label === '720') + + // eslint-disable-next-line no-param-reassign + hls.autoLevelCapping = Number(mob720?.level) + } } hls.on(Hls.Events.MANIFEST_PARSED, listener) return () => { diff --git a/src/features/StreamPlayer/index.tsx b/src/features/StreamPlayer/index.tsx index 093e90df..2e48f0f1 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -5,6 +5,7 @@ import { Name } from 'features/Name' import { FiltersPopup } from 'features/MatchSidePlaylists/components/FiltersPopup' import { WaterMark } from 'components/WaterMark' +import { AccessTimer } from 'components/AccessTimer' import { isMobileDevice } from 'config/userAgent' @@ -30,7 +31,11 @@ import RewindMobile from './components/RewindMobile' * HLS плеер, применяется на лайв и завершенных матчах */ export const StreamPlayer = (props: Props) => { - const { isOpenPopup, profile } = useMatchPageStore() + const { + access, + isOpenFiltersPopup, + profile, + } = useMatchPageStore() const { user } = useAuthStore() const { @@ -85,6 +90,7 @@ export const StreamPlayer = (props: Props) => { volumeInPercent, wrapperRef, } = useVideoPlayer(props) + return ( { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {isOpenPopup && } + {isOpenFiltersPopup && } @@ -191,6 +197,15 @@ export const StreamPlayer = (props: Props) => { selectedAudioTrack={selectedAudioTrack} /> + {!user && ( + + )} ) } diff --git a/src/features/StreamPlayer/styled.tsx b/src/features/StreamPlayer/styled.tsx index d8f54308..c096488b 100644 --- a/src/features/StreamPlayer/styled.tsx +++ b/src/features/StreamPlayer/styled.tsx @@ -96,6 +96,17 @@ export const ControlsGroup = styled.div` const supportsAspectRatio = CSS.supports('aspect-ratio', '16 / 9') && isMobileDevice export const PlayerWrapper = styled.div` + + :not(:root):fullscreen::backdrop { + position: fixed; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + background: white; + z-index: 0; + } + width: 100%; position: relative; background-color: #000; diff --git a/src/features/SystemSettings/hooks.tsx b/src/features/SystemSettings/hooks.tsx index b5204ec6..8ab7df2e 100644 --- a/src/features/SystemSettings/hooks.tsx +++ b/src/features/SystemSettings/hooks.tsx @@ -9,6 +9,8 @@ import { SELECTED_API_KEY } from 'helpers/selectedApi' import { useToggle } from 'hooks/useToggle' import { useLocalStore } from 'hooks/useStorage' +import { removeToken } from '../../helpers' +import { removeCookie } from '../../helpers/cookie' type FormElement = HTMLFormElement & { api: HTMLInputElement & { @@ -34,6 +36,8 @@ export const useSystemSettings = () => { const { api } = e.currentTarget setSelectedApi(api.value) + removeToken() + removeCookie('access_token') window.location.reload() } diff --git a/src/features/TournamentList/components/CollapseTournament/index.tsx b/src/features/TournamentList/components/CollapseTournament/index.tsx index 0b5abc1a..a796c808 100644 --- a/src/features/TournamentList/components/CollapseTournament/index.tsx +++ b/src/features/TournamentList/components/CollapseTournament/index.tsx @@ -93,7 +93,7 @@ export const CollapseTournament = ({ {countryInfo && ( diff --git a/src/features/TournamentList/components/TournamentMobile/index.tsx b/src/features/TournamentList/components/TournamentMobile/index.tsx index 2eeb55a3..69f6e0c4 100644 --- a/src/features/TournamentList/components/TournamentMobile/index.tsx +++ b/src/features/TournamentList/components/TournamentMobile/index.tsx @@ -54,7 +54,7 @@ export const TournamentMobile = ({ sport={sportType} /> )} diff --git a/src/features/TournamentPage/hooks.tsx b/src/features/TournamentPage/hooks.tsx index bf414af0..bc2ec773 100644 --- a/src/features/TournamentPage/hooks.tsx +++ b/src/features/TournamentPage/hooks.tsx @@ -18,6 +18,8 @@ import { checkUrlParams, getAllUrlParams } from 'helpers/parseUrlParams/parseUrl import { usePageParams } from 'hooks/usePageParams' import { useName } from 'features/Name' +import { useAuthStore } from 'features/AuthStore' + import { isPermittedTournament } from '../../helpers/isPermittedTournament' import { useProfileCard } from '../ProfileCard/hooks' import { useBuyMatchPopupStore } from '../BuyMatchPopup' @@ -30,6 +32,7 @@ export const useTournamentPage = () => { const { open: openBuyMatchPopup } = useBuyMatchPopupStore() const country = useName(tournamentProfile?.country || {}) const history = useHistory() + const { user } = useAuthStore() const { isFavorite, toggleFavorites } = useProfileCard() @@ -94,5 +97,6 @@ export const useTournamentPage = () => { infoItems: [country], profile, tournamentId, + user, } } diff --git a/src/features/TournamentPage/index.tsx b/src/features/TournamentPage/index.tsx index 9a0d9655..662c4793 100644 --- a/src/features/TournamentPage/index.tsx +++ b/src/features/TournamentPage/index.tsx @@ -21,6 +21,7 @@ const TournamentPage = () => { headerImage, profile, tournamentId, + user, } = useTournamentPage() return ( @@ -37,7 +38,7 @@ const TournamentPage = () => { - {(tournamentId === 131 || tournamentId === 2032) && } + {user && (tournamentId === 131 || tournamentId === 2032) && } ) } diff --git a/src/features/TournamentSubtitle/index.tsx b/src/features/TournamentSubtitle/index.tsx index 53b67a70..897db6a9 100644 --- a/src/features/TournamentSubtitle/index.tsx +++ b/src/features/TournamentSubtitle/index.tsx @@ -72,7 +72,7 @@ export const TournamentSubtitle = ({ )} - + {countryInfo && ( diff --git a/src/features/UserAccount/components/LogoutButton/index.tsx b/src/features/UserAccount/components/LogoutButton/index.tsx index 6e35fe02..30fb909d 100644 --- a/src/features/UserAccount/components/LogoutButton/index.tsx +++ b/src/features/UserAccount/components/LogoutButton/index.tsx @@ -44,7 +44,7 @@ export const LogoutButton = () => { const { logout } = useAuthStore() return ( - diff --git a/src/features/UserFavorites/TooltipBlock/index.tsx b/src/features/UserFavorites/TooltipBlock/index.tsx index c9986471..bc7c62ec 100644 --- a/src/features/UserFavorites/TooltipBlock/index.tsx +++ b/src/features/UserFavorites/TooltipBlock/index.tsx @@ -32,7 +32,7 @@ export const TooltipBlock = ({ {info?.team && }{' '} - {info?.country && } + {info?.country && } ) diff --git a/src/helpers/callApi/index.tsx b/src/helpers/callApi/index.tsx index 46f47e5c..36770e24 100644 --- a/src/helpers/callApi/index.tsx +++ b/src/helpers/callApi/index.tsx @@ -2,7 +2,6 @@ import type { CallApiArgs } from './types' import { parseJSON } from './parseJSON' import { checkStatus } from './checkStatus' import { getRequestConfig } from './getRequestConfig' -import { logoutIfUnauthorized } from './logoutIfUnauthorized' export const callApiBase = ({ abortSignal, @@ -39,7 +38,7 @@ const callApiWithTimeout = (args: CallApiArgs) => { }) .then(checkStatus) .then(parseJSON) - .catch(logoutIfUnauthorized) + // .catch(logoutIfUnauthorized) .finally(() => clearTimeout(timeoutId)) } @@ -49,5 +48,5 @@ export const callApi = (args: CallApiArgs) => { return callApiBase(args) .then(checkStatus) .then(parseJSON) - .catch(logoutIfUnauthorized) + // .catch(logoutIfUnauthorized) } diff --git a/src/helpers/cookie/index.tsx b/src/helpers/cookie/index.tsx index a14bda71..cbad2d33 100644 --- a/src/helpers/cookie/index.tsx +++ b/src/helpers/cookie/index.tsx @@ -22,6 +22,13 @@ export const removeCookie = (name: string, domain = getDomain()) => { document.cookie = `${name}=;${expires};path=/;domain=${domain}` } +export const checkCookie = (name: string) => { + const cookies = document.cookie + const token = cookies?.split('; ') + ?.filter((cookie: string) => cookie?.includes(name)) + return token[0] +} + const getDomain = () => ( process.env.NODE_ENV === 'development' ? 'localhost' diff --git a/src/helpers/languageUrlParam/index.tsx b/src/helpers/languageUrlParam/index.tsx index a3e7aa6f..98f04dd5 100644 --- a/src/helpers/languageUrlParam/index.tsx +++ b/src/helpers/languageUrlParam/index.tsx @@ -2,6 +2,7 @@ import isNull from 'lodash/isNull' import { history } from 'config/history' import { client } from 'config/clients' +import { checkCookie } from '../cookie' const KEY = 'lang' @@ -15,3 +16,10 @@ export const addLanguageUrlParam = (lang: string, url: string) => { urlObject.searchParams.set(KEY, lang) return urlObject.toString() } + +export const addAccessTokenToUrl = (url: string) => { + const urlObject = new URL(url) + const token = checkCookie('access_token')?.split('=') + token && urlObject.searchParams.set(token[0], token[1]) + return urlObject.toString() +} diff --git a/src/libs/index.ts b/src/libs/index.ts index 6195eae6..496770ad 100644 --- a/src/libs/index.ts +++ b/src/libs/index.ts @@ -2,6 +2,7 @@ export { Arrow } from './objects/Arrow' export { Boxing } from './objects/Boxing' export { Date } from './objects/Date' export { Edit } from './objects/Edit' +export { ExclamationPoint } from './objects/ExclamationPoint' export { Calendar } from './objects/Calendar' export { Check } from './objects/Check' export { CheckCircle } from './objects/CheckCircle' diff --git a/src/libs/objects/ExclamationPoint.tsx b/src/libs/objects/ExclamationPoint.tsx new file mode 100644 index 00000000..3a394c5c --- /dev/null +++ b/src/libs/objects/ExclamationPoint.tsx @@ -0,0 +1,30 @@ +export const ExclamationPoint = (): JSX.Element => ( + + + + + + +) diff --git a/src/pages/HighlightsPage/components/MatchesHighlights/index.tsx b/src/pages/HighlightsPage/components/MatchesHighlights/index.tsx index d858b131..4c164e73 100644 --- a/src/pages/HighlightsPage/components/MatchesHighlights/index.tsx +++ b/src/pages/HighlightsPage/components/MatchesHighlights/index.tsx @@ -75,7 +75,7 @@ export const MatchesHighlights = () => { {tournament.name_eng} diff --git a/src/requests/getMatches/getHomeMatches.tsx b/src/requests/getMatches/getHomeMatches.tsx index f76c9aad..b0e799c5 100644 --- a/src/requests/getMatches/getHomeMatches.tsx +++ b/src/requests/getMatches/getHomeMatches.tsx @@ -33,5 +33,6 @@ export const getHomeMatches = async ({ }, } - return requestMatches(config, url).then(getMatchesPreviews) + return requestMatches(config, url) + .then(getMatchesPreviews) } diff --git a/src/requests/getTokenVirtualUser.tsx b/src/requests/getTokenVirtualUser.tsx new file mode 100644 index 00000000..fd5b5b0f --- /dev/null +++ b/src/requests/getTokenVirtualUser.tsx @@ -0,0 +1,13 @@ +import { AUTH_SERVICE } from '../config/routes' +import { client } from '../config/clients' +import { callApi } from '../helpers' + +export const getTokenVirtualUser = async () => { + const url = `${AUTH_SERVICE}/v1/user/create?client_id=${client.auth.clientId}` + + const config = { + method: 'POST', + } + + return callApi({ config, url }) +} diff --git a/src/requests/getViewMatchDuration.tsx b/src/requests/getViewMatchDuration.tsx new file mode 100644 index 00000000..576e9d0c --- /dev/null +++ b/src/requests/getViewMatchDuration.tsx @@ -0,0 +1,43 @@ +import { + DATA_URL, + PROCEDURES, + SportTypes, +} from 'config' + +import { callApi } from 'helpers' + +const proc = PROCEDURES.get_view_user_match + +type ResponseType = { + duration?: number, + error?: string, + status: 1 | 2, +} + +type ViewMatchDurationType = { + matchId: number, + sportType: SportTypes, + userId: number, +} + +export const getViewMatchDuration = ({ + matchId, + sportType, + userId, +}: ViewMatchDurationType): Promise => { + const config = { + body: { + params: { + _p_match_id: matchId, + _p_sport_id: sportType, + _p_user_id: userId, + }, + proc, + }, + } + + return callApi({ + config, + url: DATA_URL, + }) +} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index b8074c36..e1fd2c8e 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -25,3 +25,4 @@ export * from './getPlayerPlaylists' export * from './getSubscriptions' export * from './buySubscription' export * from './saveMatchStats' +export * from './getTokenVirtualUser'