diff --git a/src/components/AccessTimer/index.tsx b/src/components/AccessTimer/index.tsx new file mode 100644 index 00000000..77bd74fd --- /dev/null +++ b/src/components/AccessTimer/index.tsx @@ -0,0 +1,124 @@ +import { useEffect, useState } 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, + onPause: () => void, + playing: boolean, +} + +const ACCESS_TIME = 60 + +export const AccessTimer = ({ + access, + isFullscreen, + onFullscreenClick, + onPause, + playing, +}: 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) + onPause() + 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..d9beec29 --- /dev/null +++ b/src/components/AccessTimer/styled.tsx @@ -0,0 +1,124 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config/userAgent' + +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/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..b07c42ad --- /dev/null +++ b/src/components/SimplePopup/index.tsx @@ -0,0 +1,65 @@ +import { 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..a332bd73 --- /dev/null +++ b/src/components/SimplePopup/styled.tsx @@ -0,0 +1,150 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config/userAgent' + +import { ModalWindow } from 'features/Modal/styled' +import { + ApplyButton, + Body, + Modal as BaseModal, + HeaderTitle, +} from 'features/AuthServiceApp/components/RegisterPopup/styled' + +import { client } from 'features/AuthServiceApp/config/clients/index' +import { Header as BaseHeader } from '../../features/PopupComponents' + +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; + + @media screen and (orientation: landscape) { + } + ` + : ''}; +` 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..a7386b98 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' @@ -9,39 +13,30 @@ import { matomoInstance } from 'config/matomo' import { isAvailable } from 'config/env' 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 { readToken } from '../../helpers' +import { useAuthStore } from '../AuthStore' setClientTitleAndDescription(client.title, client.description) const Main = () => { - const { loadingUser, user } = useAuthStore() + const [isToken, setIsToken] = useState(false) + const { userInfo } = useAuthStore() - if (!user && (isMatchPage() || checkPage(PAGES.tournament))) return - if (!user && isMatchPageRFEF()) return - - 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 +52,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/RegisterPopup/styled.tsx b/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx index a661f487..83e06bb0 100644 --- a/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx +++ b/src/features/AuthServiceApp/components/RegisterPopup/styled.tsx @@ -52,9 +52,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/auth.tsx b/src/features/AuthServiceApp/requests/auth.tsx index 612e48e3..f6559430 100644 --- a/src/features/AuthServiceApp/requests/auth.tsx +++ b/src/features/AuthServiceApp/requests/auth.tsx @@ -1,6 +1,7 @@ import { getApiUrl } from '../config/routes' import type { UrlParams } from './register' +import { checkCookie } from '../../../helpers/cookie' const errorLexics = { 1: 'error_invalid_email_or_password', @@ -41,7 +42,9 @@ export const loginCheck = async ({ }), method: 'POST', } - const response = await fetch(url, init) + const response = await fetch(`${url}${checkCookie('access_token') + ? `&${checkCookie('access_token')}` + : ''}`, init) const body: SuccessResponse | FailedResponse = await response.json() diff --git a/src/features/AuthServiceApp/requests/register.tsx b/src/features/AuthServiceApp/requests/register.tsx index 7573b348..9b58ace4 100644 --- a/src/features/AuthServiceApp/requests/register.tsx +++ b/src/features/AuthServiceApp/requests/register.tsx @@ -1,6 +1,7 @@ import type { ClientIds } from 'config/clients/types' import { getApiUrl } from 'features/AuthServiceApp/config/routes' +import { checkCookie } from '../../../helpers/cookie' const errorLexics = { 1: 'error_invalid_email_or_password', @@ -45,8 +46,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 +59,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/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/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 && !matchProfile.sub && !user) { if (matchProfile && ( !matchProfile.sub || (checkUrlParams('subscribe') @@ -35,6 +38,7 @@ export const SubscriptionGuard = ({ children }: Props) => { openBuyMatchPopup, matchProfile, sportType, + user, ]) return ( diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 184f47e9..015465ea 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -12,6 +12,7 @@ import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' 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' @@ -22,11 +23,18 @@ import { useTournamentData } from './useTournamentData' import { useMatchData } from './useMatchData' import { useFiltersPopup } from './useFitersPopup' import { useTabEvents } from './useTabEvents' +import { useAuthStore } from '../../../AuthStore' + +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, @@ -42,7 +50,7 @@ export const useMatchPage = () => { countOfFilters, filters, isEmptyFilters, - isOpen: isOpenPopup, + isOpen: isOpenFiltersPopup, resetEvents, resetPlayers, toggle: togglePopup, @@ -62,7 +70,37 @@ export const useMatchPage = () => { ) } return () => clearInterval(getIntervalMatch) - }, [matchProfile, sportType, matchId]) + }, [ + matchProfile, + sportType, + matchId]) + + useEffect(() => { + if (user) return + + const getIntervalMatch: ReturnType = setInterval( + () => getViewMatchDuration({ + matchId, + sportType, + userId: Number(userInfo?.email), + }).then(({ + duration, + error, + }) => { + if (error || (duration && Number(duration) > ACCESS_TIME)) { + setAccess(false) + } + }), 1000 * 5, + ) + // eslint-disable-next-line + return () => clearInterval(getIntervalMatch) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + user, + matchProfile, + sportType, + matchId, + ]) useEffect(() => { let getIntervalMatch: ReturnType @@ -159,6 +197,7 @@ export const useMatchPage = () => { } return { + access, activeEvents, activeFirstTeamPlayers, activeSecondTeamPlayers, @@ -173,7 +212,7 @@ export const useMatchPage = () => { hideProfileCard, isEmptyFilters, isLiveMatch, - isOpenPopup, + isOpenFiltersPopup, isPlayFilterEpisodes, isStarted, likeImage, diff --git a/src/features/Matches/helpers/getMatchClickAction/index.tsx b/src/features/Matches/helpers/getMatchClickAction/index.tsx index 12f9e9b6..1d653b0d 100644 --- a/src/features/Matches/helpers/getMatchClickAction/index.tsx +++ b/src/features/Matches/helpers/getMatchClickAction/index.tsx @@ -10,11 +10,8 @@ export enum MatchAccess { export const getMatchAccess = ({ access, - calc, date, - has_video, live, - storage, sub, }: Match) => { const dateToMs = Date.parse(date?.replace(/ /, 'T')) // без замены не будет работать в сафари diff --git a/src/features/Menu/index.tsx b/src/features/Menu/index.tsx index 3cba50bb..7d1bc684 100644 --- a/src/features/Menu/index.tsx +++ b/src/features/Menu/index.tsx @@ -14,10 +14,13 @@ import { Icon, } from './styled' +import { useAuthStore } from '../AuthStore' + 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..d000d909 100644 --- a/src/features/MultiSourcePlayer/index.tsx +++ b/src/features/MultiSourcePlayer/index.tsx @@ -21,9 +21,16 @@ import type { Props } from './hooks' import { useMultiSourcePlayer } from './hooks' import { Players } from './types' import { REWIND_SECONDS } from './config' +import { AccessTimer } from '../../components/AccessTimer' +import { useAuthStore } from '../AuthStore' export const MultiSourcePlayer = (props: Props) => { - const { isOpenPopup, profile } = props + const { + access, + isOpenPopup, + profile, + } = props + const { activeChapterIndex, activePlayer, @@ -78,6 +85,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/StreamPlayer/components/YoutubePlayer/index.tsx b/src/features/StreamPlayer/components/YoutubePlayer/index.tsx index 75c2b2bd..99815ad5 100644 --- a/src/features/StreamPlayer/components/YoutubePlayer/index.tsx +++ b/src/features/StreamPlayer/components/YoutubePlayer/index.tsx @@ -7,7 +7,7 @@ import { PlayerWrapper } from '../../styled' import { useVideoPlayer, Props } from '../../hooks' export const YoutubePlayer = (props: Props) => { - 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/index.tsx b/src/features/StreamPlayer/index.tsx index 093e90df..261f4c01 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -25,12 +25,17 @@ import { useVideoPlayer } from './hooks' import { useAuthStore } from '../AuthStore' import { Controls } from './components/Controls' import RewindMobile from './components/RewindMobile' +import { AccessTimer } from '../../components/AccessTimer' /** * HLS плеер, применяется на лайв и завершенных матчах */ export const StreamPlayer = (props: Props) => { - const { isOpenPopup, profile } = useMatchPageStore() + const { + access, + isOpenFiltersPopup, + profile, + } = useMatchPageStore() const { user } = useAuthStore() const { @@ -96,7 +101,7 @@ export const StreamPlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {isOpenPopup && } + {isOpenFiltersPopup && } @@ -191,6 +196,13 @@ 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/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/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..af9f563c 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/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/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..5920aaeb --- /dev/null +++ b/src/requests/getTokenVirtualUser.tsx @@ -0,0 +1,16 @@ +import { AUTH_SERVICE } from '../config/routes' +import { client } from '../config/clients' + +export const getTokenVirtualUser = async () => { + const url = `${AUTH_SERVICE}/v1/user/create?client_id=${client.auth.clientId}` + + const config = { + method: 'POST', + } + + const response = await fetch(url, config) + + const body = await response.json() + + return body +} 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'