diff --git a/public/images/exclamationInCircle.svg b/public/images/exclamationInCircle.svg
new file mode 100644
index 00000000..68bba8bf
--- /dev/null
+++ b/public/images/exclamationInCircle.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx
index 06dad8e7..f58b3483 100644
--- a/src/config/lexics/indexLexics.tsx
+++ b/src/config/lexics/indexLexics.tsx
@@ -91,6 +91,13 @@ const sportsPopup = {
hockey_popup: 15217,
}
+const newDevicePopup = {
+ log_with_one_device: 19599,
+ logged_another_device: 19597,
+ logged_out_soon: 19596,
+ ok: 724,
+}
+
export const indexLexics = {
add_to_favorites: 14967,
add_to_favorites_error: 12943,
@@ -161,6 +168,7 @@ export const indexLexics = {
...preferencesPopupLexics,
...proceduresLexics,
...matchPopupLexics,
+ ...newDevicePopup,
...buyMatchPopupLexics,
...publicLexics,
...paymentLexics,
diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx
index 26ba2ccb..b25f8484 100644
--- a/src/features/App/AuthenticatedApp.tsx
+++ b/src/features/App/AuthenticatedApp.tsx
@@ -27,6 +27,7 @@ import { TournamentsPopup } from 'features/TournamentsPopup'
import { TournamentPopupStore } from 'features/TournamentsPopup/store'
import { CardsStore } from 'features/CardsStore'
import { NoNetworkPopup, NoNetworkPopupStore } from 'features/NoNetworkPopup'
+import { NewDevicePopup } from 'features/NewDevicePopup'
const HomePage = lazy(() => import('features/HomePage'))
const UserAccount = lazy(() => import('features/UserAccount'))
@@ -43,6 +44,7 @@ export const AuthenticatedApp = () => {
useLexicsConfig(indexLexics)
return (
+
diff --git a/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
index ae9525e5..7201d339 100644
--- a/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
+++ b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
@@ -29,7 +29,7 @@ export const useChangePasswordForm = () => {
await changePassword(client_id, password)
setError('')
setIsFetching(false)
- history.push(`/autorize?client_id=${client_id}`)
+ history.push(`/authorize?client_id=${client_id}`)
} catch (err) {
setError(String(err))
setIsFetching(false)
diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx
index 24801bdd..2d7d0cb2 100644
--- a/src/features/AuthStore/hooks/useAuth.tsx
+++ b/src/features/AuthStore/hooks/useAuth.tsx
@@ -16,7 +16,11 @@ import { PAGES } from 'config'
import {
addLanguageUrlParam,
} from 'helpers/languageUrlParam'
-import { writeToken, removeToken } from 'helpers/token'
+import {
+ writeToken,
+ removeToken,
+ readToken,
+} from 'helpers/token'
import { setCookie, removeCookie } from 'helpers/cookie'
import { isMatchPage } from 'helpers/isMatchPage'
@@ -27,7 +31,8 @@ import {
queryParamStorage,
} from 'features/QueryParamsStorage'
-import { getUserInfo } from 'requests'
+import { getUserInfo } from 'requests/getUserInfo'
+import { checkDevice, FailedResponse } from 'requests/checkDevice'
import { getClientSettings } from '../helpers'
@@ -39,6 +44,7 @@ export const useAuth = () => {
isOpen: loadingUser,
} = useToggle(true)
const [user, setUser] = useState()
+ const [isNewDeviceLogin, setIsNewDeviceLogin] = useState(false)
const userManager = useMemo(() => new UserManager(getClientSettings()), [])
const login = useCallback(async () => (
@@ -142,6 +148,27 @@ export const useAuth = () => {
history,
])
+ const checkNewDevice = useCallback(async () => {
+ const accessToken = readToken()
+ if (!accessToken) return
+
+ await checkDevice(accessToken).catch((er:FailedResponse) => {
+ if (er.error) {
+ logout()
+ return
+ }
+ userManager.clearStaleState()
+ setIsNewDeviceLogin(true)
+ setTimeout(logout, 10000)
+ })
+ }, [logout, userManager])
+
+ useEffect(() => {
+ const startCheckDevice = setInterval(checkNewDevice, 20000)
+ isNewDeviceLogin && clearInterval(startCheckDevice)
+ return () => clearInterval(startCheckDevice)
+ }, [checkNewDevice, isNewDeviceLogin, setIsNewDeviceLogin])
+
useEffect(() => {
// попытаемся обновить токен используя refresh_token
const tryRenewToken = () => {
@@ -177,11 +204,13 @@ export const useAuth = () => {
}, [fetchUserInfo, user])
const auth = useMemo(() => ({
+ isNewDeviceLogin,
loadingUser,
login,
logout,
user,
}), [
+ isNewDeviceLogin,
logout,
user,
login,
diff --git a/src/features/NewDevicePopup/components/Notification/index.tsx b/src/features/NewDevicePopup/components/Notification/index.tsx
new file mode 100644
index 00000000..6895f64f
--- /dev/null
+++ b/src/features/NewDevicePopup/components/Notification/index.tsx
@@ -0,0 +1,23 @@
+import { T9n } from 'features/T9n'
+import {
+ NotificationWrapper,
+ Title,
+ Text,
+ ExclamationInCircle,
+} from '../../styled'
+
+export const Notification = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/src/features/NewDevicePopup/hooks/index.tsx b/src/features/NewDevicePopup/hooks/index.tsx
new file mode 100644
index 00000000..1d6d6543
--- /dev/null
+++ b/src/features/NewDevicePopup/hooks/index.tsx
@@ -0,0 +1,23 @@
+import { useAuthStore } from 'features/AuthStore'
+import { useEffect, useState } from 'react'
+
+export const useNewDevicePopup = () => {
+ const { isNewDeviceLogin } = useAuthStore()
+
+ const [isOpen, setIsOpen] = useState(isNewDeviceLogin)
+
+ useEffect(() => {
+ setIsOpen(isNewDeviceLogin)
+ }, [isNewDeviceLogin])
+
+ const close = () => {
+ setIsOpen(false)
+ }
+
+ return {
+ close,
+ isOpen,
+ setIsOpen,
+
+ }
+}
diff --git a/src/features/NewDevicePopup/index.tsx b/src/features/NewDevicePopup/index.tsx
new file mode 100644
index 00000000..9d147e16
--- /dev/null
+++ b/src/features/NewDevicePopup/index.tsx
@@ -0,0 +1,32 @@
+import { T9n } from 'features/T9n'
+
+import { Notification } from './components/Notification'
+import { useNewDevicePopup } from './hooks'
+
+import {
+ Button,
+ Modal,
+ Wrapper,
+} from './styled'
+
+export const NewDevicePopup = () => {
+ const {
+ close,
+ isOpen,
+ } = useNewDevicePopup()
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/src/features/NewDevicePopup/styled.tsx b/src/features/NewDevicePopup/styled.tsx
new file mode 100644
index 00000000..5e16b0a5
--- /dev/null
+++ b/src/features/NewDevicePopup/styled.tsx
@@ -0,0 +1,107 @@
+import styled, { css } from 'styled-components/macro'
+
+import { isMobileDevice } from 'config/userAgent'
+import { Modal as BaseModal } from 'features/Modal'
+import { ButtonSolid } from 'features/Common/Button'
+import { ModalWindow } from 'features/Modal/styled'
+
+export const Modal = styled(BaseModal)`
+ background-color: rgba(0, 0, 0, 0.7);
+ text-align: center;
+
+ ${ModalWindow} {
+ border-radius: 4px;
+ padding: 60px 54px 50px;
+ max-width: 700px;
+
+ ${isMobileDevice
+ ? css`
+ width: calc(100vw - 24px);
+ padding: 38px 20px 24px;
+ @media screen and (orientation: landscape){
+ padding: 20px;
+ }
+ `
+ : ''};
+ }
+`
+
+export const Wrapper = styled.div``
+
+export const NotificationWrapper = styled.div`
+ width: 100%;
+`
+
+export const Title = styled.div`
+ font-style: normal;
+ font-size: 24px;
+ font-weight: 700;
+ margin-bottom: 30px;
+ ${isMobileDevice
+ ? css`
+ font-size: 20px;
+ @media screen and (orientation: landscape){
+ font-size: 17px;
+ margin-bottom: 20px;
+ }
+ }
+ `
+ : ''};
+`
+
+export const Text = styled.p`
+ font-style: normal;
+ font-weight: normal;
+ font-size: 20px;
+ text-align: center;
+ margin-bottom: 30px;
+ ${isMobileDevice
+ ? css`
+ font-size: 14px;
+ line-height: 1.5;
+ margin-bottom: 20px;
+ `
+ : ''};
+`
+
+export const Button = styled(ButtonSolid)`
+ min-width: 134px;
+ width: auto;
+ height: 50px;
+ padding: 0 20px;
+ background-color: ${({ theme: { colors } }) => colors.button};
+ color: ${({ theme: { colors } }) => colors.white};
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
+ font-weight: 600;
+ font-size: 20px;
+ ${isMobileDevice
+ ? css`
+ height: 50px;
+ width: 100%;
+ font-size: 12px;
+ @media screen and (orientation: landscape){
+ max-width: 290px;
+ }
+ `
+ : ''};
+
+
+`
+
+export const ExclamationInCircle = styled.div`
+ width: 88px;
+ height: 88px;
+ background: url(/images/exclamationInCircle.svg) no-repeat;
+ background-size: cover;
+ margin: 0 auto 35px;
+ ${isMobileDevice
+ ? css`
+ @media screen and (orientation: landscape){
+ width: 40px;
+ height: 40px;
+ margin: 0 auto 15px;
+ }
+ `
+ : ''};
+`
diff --git a/src/requests/checkDevice.tsx b/src/requests/checkDevice.tsx
new file mode 100644
index 00000000..905cac6a
--- /dev/null
+++ b/src/requests/checkDevice.tsx
@@ -0,0 +1,26 @@
+import { AUTH_SERVICE } from '../config/routes'
+
+export type FailedResponse = {
+ error?: string,
+ ok: false,
+}
+
+export type SuccessResponse = {
+ ok: true,
+}
+
+export const checkDevice = async (token: string) => {
+ const url = `${AUTH_SERVICE}/authorize/check-device?access_token=${token}`
+
+ const config = {
+ method: 'GET',
+ }
+
+ const response = await fetch(url, config)
+
+ const body: SuccessResponse | FailedResponse = await response.json()
+
+ if (body.ok) return Promise.resolve()
+
+ return Promise.reject(body)
+}