feat(#175): access to platform without authorization

pull/39/head
Andrei Dekterev 3 years ago
parent e3f4f737b2
commit 20d5c3eae4
  1. 124
      src/components/AccessTimer/index.tsx
  2. 124
      src/components/AccessTimer/styled.tsx
  3. 5
      src/components/PictureInPicture/PiP.tsx
  4. 65
      src/components/SimplePopup/index.tsx
  5. 150
      src/components/SimplePopup/styled.tsx
  6. 8
      src/config/lexics/indexLexics.tsx
  7. 1
      src/config/procedures.tsx
  8. 35
      src/features/App/index.tsx
  9. 5
      src/features/AuthServiceApp/components/RegisterPopup/styled.tsx
  10. 5
      src/features/AuthServiceApp/requests/auth.tsx
  11. 8
      src/features/AuthServiceApp/requests/register.tsx
  12. 78
      src/features/AuthStore/hooks/useAuth.tsx
  13. 26
      src/features/HomePage/hooks.tsx
  14. 9
      src/features/MatchPage/components/FinishedMatch/index.tsx
  15. 4
      src/features/MatchPage/components/SubscriptionGuard/index.tsx
  16. 45
      src/features/MatchPage/store/hooks/index.tsx
  17. 3
      src/features/Matches/helpers/getMatchClickAction/index.tsx
  18. 13
      src/features/Menu/index.tsx
  19. 2
      src/features/Modal/styled.tsx
  20. 24
      src/features/MultiSourcePlayer/hooks/index.tsx
  21. 19
      src/features/MultiSourcePlayer/index.tsx
  22. 4
      src/features/StreamPlayer/components/YoutubePlayer/index.tsx
  23. 19
      src/features/StreamPlayer/hooks/index.tsx
  24. 16
      src/features/StreamPlayer/index.tsx
  25. 11
      src/features/StreamPlayer/styled.tsx
  26. 2
      src/features/UserAccount/components/LogoutButton/index.tsx
  27. 5
      src/helpers/callApi/index.tsx
  28. 7
      src/helpers/cookie/index.tsx
  29. 1
      src/libs/index.ts
  30. 30
      src/libs/objects/ExclamationPoint.tsx
  31. 3
      src/requests/getMatches/getHomeMatches.tsx
  32. 16
      src/requests/getTokenVirtualUser.tsx
  33. 43
      src/requests/getViewMatchDuration.tsx
  34. 1
      src/requests/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<typeof setInterval> | 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
? (
<AccessTimerContainer isFullscreen={isFullscreen}>
<TimerContainer>
<PreviewInfo>
<Timer>{secondsToHms(time)}&nbsp;</Timer>
<T9n t='game_preview' className='gamePreview' />
</PreviewInfo>
<SignText>
<T9n t='sign_in_full_game' />
</SignText>
</TimerContainer>
<SignInBtn onClick={() => logout('saveToken')}>
<T9n t='sign_in' />
</SignInBtn>
</AccessTimerContainer>
)
: (
<SimplePopup
isModalOpen={timeIsFinished}
withCloseButton={false}
headerName='sec_60'
mainText='continue_watching'
buttonName='sign_in'
onHandle={() => logout('saveToken')}
icon={<Icon refIcon='ExclamationPoint' />}
/>
)}
</>
)
}

@ -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
`
: ''};
`

@ -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 (
<PipWrapper>

@ -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 (
<Modal
isOpen={isModalOpen}
withCloseButton={withCloseButton}
>
<Wrapper>
<Header>
{icon}
<ScHeaderTitle>
<T9n t={headerName ?? ''} />
</ScHeaderTitle>
</Header>
<ScBody>
<ScText>
<T9n t={mainText} />
</ScText>
</ScBody>
{buttonName
&& (
<Footer>
<ScApplyButton onClick={onHandle}>
<T9n t={buttonName} />
</ScApplyButton>
</Footer>
)}
</Wrapper>
</Modal>
)
}

@ -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) {
}
`
: ''};
`

@ -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,

@ -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',

@ -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 <JoinMatchPage />
if (!user && isMatchPageRFEF()) return <JoinMatchPageRFEF />
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 <AuthenticatedApp />
return isToken ? <AuthenticatedApp /> : null
}
const date = new Date()
@ -57,8 +52,8 @@ const OTTApp = () => (
<Background>
<Suspense fallback={null}>
{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())
? (<Main />) : (<UnavailableText />)}
</Suspense>
</Background>

@ -52,9 +52,12 @@ export const Wrapper = styled.div<WrapperProps>`
`
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}{

@ -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()

@ -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()

@ -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<UserInfo>()
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

@ -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(() => {

@ -17,7 +17,11 @@ import { useMatchPageStore } from '../../store'
export const FinishedMatch = () => {
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation)
const { isOpenPopup, profile } = useMatchPageStore()
const {
access,
isOpenFiltersPopup,
profile,
} = useMatchPageStore()
const {
chapters,
closeSettingsPopup,
@ -47,8 +51,9 @@ export const FinishedMatch = () => {
{!isEmpty(chapters) && (
<Fragment>
<MultiSourcePlayer
access={access}
setCircleAnimation={setCircleAnimation}
isOpenPopup={isOpenPopup}
isOpenPopup={isOpenFiltersPopup}
chapters={chapters}
onPlayingChange={onPlayingChange}
profile={profile}

@ -7,6 +7,7 @@ import { useBuyMatchPopupStore } from 'features/BuyMatchPopup'
import { useMatchPageStore } from 'features/MatchPage/store'
import { prepareMatchProfile } from '../../helpers/prepareMatchProfile'
import { useAuthStore } from '../../../AuthStore'
import { checkUrlParams, getAllUrlParams } from '../../../../helpers/parseUrlParams/parseUrlParams'
type Props = {
@ -17,8 +18,10 @@ export const SubscriptionGuard = ({ children }: Props) => {
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 (

@ -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<MatchInfo>(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<typeof setInterval> = 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<typeof setInterval>
@ -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,

@ -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')) // без замены не будет работать в сафари

@ -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 (
<MenuList>
@ -29,8 +32,14 @@ export const Menu = () => {
</MenuItem>
)
}
<MenuItem>
<Link to={`${PAGES.useraccount}/personal-info`}>
<MenuItem onClick={(e) => {
e.preventDefault()
!user && logout('saveToken')
}}
>
<Link
to={`${PAGES.useraccount}/personal-info`}
>
<Icon src='userAccount' size='0.95rem' />
</Link>
</MenuItem>

@ -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;
`

@ -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,

@ -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 (
<PlayerWrapper
@ -210,6 +218,15 @@ export const MultiSourcePlayer = (props: Props) => {
volumeInPercent={volumeInPercent}
/>
<ControlsGradient isVisible={mainControlsVisible} />
{!user && (
<AccessTimer
access={access}
isFullscreen={isFullscreen}
onFullscreenClick={onFullscreenClick}
playing={ready && playing && !!playedProgress}
onPause={onPause}
/>
)}
</PlayerWrapper>
)
}

@ -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 && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<YouTube
videoId={key}
opts={{

@ -446,11 +446,13 @@ export const useVideoPlayer = ({
// ведем статистику просмотра матча
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,
@ -462,7 +464,12 @@ export const useVideoPlayer = ({
} else {
stopCollectingStats()
}
}, [playing, startCollectingStats, stopCollectingStats])
}, [
playing,
startCollectingStats,
stopCollectingStats,
profileId,
])
return {
activeChapterIndex,

@ -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 && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<LoaderWrapper buffering={buffering}>
<Loader color='#515151' />
</LoaderWrapper>
@ -191,6 +196,13 @@ export const StreamPlayer = (props: Props) => {
selectedAudioTrack={selectedAudioTrack}
/>
<ControlsGradient isVisible={mainControlsVisible} />
!user && <AccessTimer
access={access}
isFullscreen={isFullscreen}
onFullscreenClick={onFullscreenClick}
playing={ready && playing && !!playedProgress}
onPause={onPause}
/>
</PlayerWrapper>
)
}

@ -96,6 +96,17 @@ export const ControlsGroup = styled.div`
const supportsAspectRatio = CSS.supports('aspect-ratio', '16 / 9') && isMobileDevice
export const PlayerWrapper = styled.div<PlayStopProps>`
: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;

@ -44,7 +44,7 @@ export const LogoutButton = () => {
const { logout } = useAuthStore()
return (
<Button onClick={logout}>
<Button onClick={() => logout()}>
<ExitIcon />
<T9n t='logout' />
</Button>

@ -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)
}

@ -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'

@ -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'

@ -0,0 +1,30 @@
export const ExclamationPoint = (): JSX.Element => (
<svg
xmlns='http://www.w3.org/2000/svg'
width='88'
height='88'
fill='none'
viewBox='0 0 88 88'
>
<circle cx='44' cy='44' r='44' fill='#fff' />
<rect
width='3'
height='30'
x='45'
y='56'
fill='#000'
rx='1.5'
transform='rotate(-180 45 56)'
/>
<rect
width='3'
height='30'
x='45'
y='56'
fill='#000'
rx='1.5'
transform='rotate(-180 45 56)'
/>
<circle cx='43.5' cy='60.5' r='2.5' fill='#000' />
</svg>
)

@ -33,5 +33,6 @@ export const getHomeMatches = async ({
},
}
return requestMatches(config, url).then(getMatchesPreviews)
return requestMatches(config, url)
.then(getMatchesPreviews)
}

@ -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
}

@ -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<ResponseType> => {
const config = {
body: {
params: {
_p_match_id: matchId,
_p_sport_id: sportType,
_p_user_id: userId,
},
proc,
},
}
return callApi({
config,
url: DATA_URL,
})
}

@ -25,3 +25,4 @@ export * from './getPlayerPlaylists'
export * from './getSubscriptions'
export * from './buySubscription'
export * from './saveMatchStats'
export * from './getTokenVirtualUser'

Loading…
Cancel
Save