Ott 1725 auth service (#575)

* Ott 1725 part 1 (#528)

* chore(1725): configured auth service app

* feat(1725): new LanguageSelect component

* feat(1725): restored old components

* Ott 1725 part 2 (#545)

* feat(1725): login page

* feat(1725): registration page

* fix(1725): review fix

* Ott 1725 part 3 (#547)

* fix(1725): refreshing token

* fix(1725): added logo and background client configs

* fix(1725): sync lang change (#548)

* Ott 1725 part 5 (#549)

* fix(1725): setting icons, title, desc in head tag

* fix(1725): added lexics

* fix(1725): facr input and button customizations

* Ott 1725 part 6 (#560)

* fix(1725): scale on mobile

* refactor(1725): fixed scrollbar issue when refreshing token

* refactor(1725): removed token refreshing

* fix(1725): fix build command and redirecting

* refactor(1725): login check and final touches

* Ott 1956 register popup (#574)

* Ott 1725 part 1 (#528)

* chore(1725): configured auth service app

* feat(1725): new LanguageSelect component

* feat(1725): restored old components

* feat(#1956): add popup on register page

* refactor(#1956): fix makefile

* feat(#1956): add popup on register page

* refactor(#1956): fix makefile

* refactor(#1956): fix comments

* refactor(#1956): fix comments

* Update index.tsx

Co-authored-by: Mirlan <m.maksitaliev@gmail.com>

Co-authored-by: Andrei Dekterev <57942757+dekterev@users.noreply.github.com>
keep-around/100b3f5493bc24e9889c376dcfc797564582c41c
Mirlan 4 years ago committed by GitHub
parent 1b9f0f8251
commit f692b42d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 11
      Makefile
  3. 5
      package.json
  4. 2
      public/clients/facr/desc/index.js
  5. 2
      public/clients/instat/desc/index.js
  6. BIN
      public/images/facr_auth_logo.png
  7. 60
      public/index.html
  8. 1147
      public/terms-and-conditions.html
  9. 6
      src/config/clients/facr.tsx
  10. 6
      src/config/clients/instat.tsx
  11. 9
      src/config/clients/types.tsx
  12. 2
      src/config/userAgent.tsx
  13. 10
      src/features/App/index.tsx
  14. 2
      src/features/ArrowLoader/styled.tsx
  15. 51
      src/features/AuthServiceApp/components/App/index.tsx
  16. 11
      src/features/AuthServiceApp/components/Input/index.tsx
  17. 48
      src/features/AuthServiceApp/components/Input/styled.tsx
  18. 32
      src/features/AuthServiceApp/components/LanguageSelect/hooks.tsx
  19. 70
      src/features/AuthServiceApp/components/LanguageSelect/index.tsx
  20. 147
      src/features/AuthServiceApp/components/LanguageSelect/styled.tsx
  21. 75
      src/features/AuthServiceApp/components/Login/hooks.tsx
  22. 96
      src/features/AuthServiceApp/components/Login/index.tsx
  23. 31
      src/features/AuthServiceApp/components/Login/styled.tsx
  24. 27
      src/features/AuthServiceApp/components/Logo/index.tsx
  25. 64
      src/features/AuthServiceApp/components/PasswordInput/index.tsx
  26. 63
      src/features/AuthServiceApp/components/RegisterPopup/index.tsx
  27. 164
      src/features/AuthServiceApp/components/RegisterPopup/styled.tsx
  28. 72
      src/features/AuthServiceApp/components/Registration/hooks.tsx
  29. 125
      src/features/AuthServiceApp/components/Registration/index.tsx
  30. 43
      src/features/AuthServiceApp/components/Registration/styled.tsx
  31. 40
      src/features/AuthServiceApp/config/clients/facr.tsx
  32. 13
      src/features/AuthServiceApp/config/clients/index.tsx
  33. 20
      src/features/AuthServiceApp/config/clients/instat.tsx
  34. 20
      src/features/AuthServiceApp/config/clients/types.tsx
  35. 32
      src/features/AuthServiceApp/config/lexics.tsx
  36. 4
      src/features/AuthServiceApp/config/pages.tsx
  37. 13
      src/features/AuthServiceApp/config/routes.tsx
  38. 22
      src/features/AuthServiceApp/helpers/isValidEmail/__tests__/index.tsx
  39. 9
      src/features/AuthServiceApp/helpers/isValidEmail/index.tsx
  40. 0
      src/features/AuthServiceApp/helpers/isValidPassword/__tests__/index.tsx
  41. 0
      src/features/AuthServiceApp/helpers/isValidPassword/index.tsx
  42. 58
      src/features/AuthServiceApp/hooks/useAuthFields.tsx
  43. 35
      src/features/AuthServiceApp/index.tsx
  44. 36
      src/features/AuthServiceApp/requests/auth.tsx
  45. 49
      src/features/AuthServiceApp/requests/register.tsx
  46. 125
      src/features/AuthServiceApp/styled.tsx
  47. 12
      src/features/AuthStore/hooks/useAuth.tsx
  48. 14
      src/features/Common/InputGroup/index.tsx
  49. 3
      src/features/Common/index.tsx
  50. 13
      src/features/GlobalStores/index.tsx
  51. 1
      src/features/GlobalStyles/index.tsx
  52. 6
      src/features/LexicsStore/hooks/index.tsx
  53. 7
      src/features/LexicsStore/hooks/useLang.tsx
  54. 11
      src/features/LexicsStore/index.tsx
  55. 14
      src/helpers/languageUrlParam/index.tsx
  56. 38
      src/helpers/setClientHeads/index.tsx
  57. 6
      src/hooks/useStorage/index.tsx
  58. 16
      src/index.tsx
  59. 1
      src/react-app-env.d.ts

1
.gitignore vendored

@ -10,6 +10,7 @@
# production # production
/build /build
/build_auth
# misc # misc
.DS_Store .DS_Store

@ -16,6 +16,16 @@ preproduction-build: clean
production-build: clean production-build: clean
REACT_APP_ENV=production REACT_APP_CLIENT=instat REACT_APP_STRIPE_PK=pk_live_ANI76cBhSo69DZUxPmyRVIZW npm run build REACT_APP_ENV=production REACT_APP_CLIENT=instat REACT_APP_STRIPE_PK=pk_live_ANI76cBhSo69DZUxPmyRVIZW npm run build
auth-build:
rm -rf build_auth
REACT_APP_ENV=staging BUILD_PATH=build_auth GENERATE_SOURCEMAP=false npm run build:auth
npx gzipper --verbose build_auth
auth-production-build:
rm -rf build_auth
REACT_APP_ENV=production BUILD_PATH=build_auth GENERATE_SOURCEMAP=false npm run build:auth
npx gzipper --verbose build_auth
facr-build: clean facr-build: clean
REACT_APP_ENV=staging REACT_APP_CLIENT=facr npm run build REACT_APP_ENV=staging REACT_APP_CLIENT=facr npm run build
@ -35,6 +45,7 @@ preprod: preproduction-build
facr-prod: facr-production-build facr-prod: facr-production-build
rsync -zavP build/ -e 'ssh -p 666' ott@de.instat.tv:/usr/local/www/ott/facr-wwwroot/ rsync -zavP build/ -e 'ssh -p 666' ott@de.instat.tv:/usr/local/www/ott/facr-wwwroot/
rsync -zavP build/ -e 'ssh -p 666' ott@fr.instat.tv:/usr/local/www/ott/facr-wwwroot/ rsync -zavP build/ -e 'ssh -p 666' ott@fr.instat.tv:/usr/local/www/ott/facr-wwwroot/
rsync -zavP build/ -e 'ssh -p 666' ott@bkup.instat.tv:/usr/local/www/ott/facr-wwwroot/
stage: build stage: build
rsync -zavP build/ -e 'ssh -p 666' ott-staging@staging.instat.tv:/usr/local/www/ott-staging/wwwroot/ rsync -zavP build/ -e 'ssh -p 666' ott-staging@staging.instat.tv:/usr/local/www/ott-staging/wwwroot/

@ -11,7 +11,10 @@
"lint": "eslint \"src/**/*.{ts,tsx}\"", "lint": "eslint \"src/**/*.{ts,tsx}\"",
"stylelint": "stylelint \"src/**/*.{ts,tsx}\"", "stylelint": "stylelint \"src/**/*.{ts,tsx}\"",
"storybook": "start-storybook -p 9009 -s public", "storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public" "build-storybook": "build-storybook -s public",
"start:auth": "REACT_APP_TYPE=auth-service react-scripts start",
"build:auth": "REACT_APP_TYPE=auth-service react-scripts build",
"test:auth": "REACT_APP_TYPE=auth-service react-scripts test"
}, },
"dependencies": { "dependencies": {
"@stripe/react-stripe-js": "^1.4.0", "@stripe/react-stripe-js": "^1.4.0",

@ -1,2 +0,0 @@
const title = 'FACR.TV - The home of Czech football streaming'
const desc = 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices'

@ -1,2 +0,0 @@
const title = 'InStat TV - The Home of Sports Streaming'
const desc = 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.'

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -2,46 +2,42 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link
rel="icon"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon.ico"
/>
<link
rel="apple-touch-icon"
type="image/png"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon-16x16.png"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1 user-scalable=0"
/>
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/manifest.json" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1 user-scalable=0"
/>
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link <link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<script src="https://js.stripe.com/v3" async></script>
<meta name="description" content=""/> <meta name="description" content=""/>
<title></title> <title></title>
<script src="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/desc/index.js"></script> <% if (process.env.REACT_APP_TYPE === 'ott') { %>
<script type="text/javascript"> <link
window.document.title = title rel="icon"
window.document.all.description.content = desc href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon.ico"
</script> />
<link
rel="apple-touch-icon"
type="image/png"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/clients/%REACT_APP_CLIENT%/favicon/manifest.json" />
<% } %>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ import { css } from 'styled-components/macro'
import { PROCEDURES } from '../procedures' import { PROCEDURES } from '../procedures'
import type { ClientConfig } from './types' import { ClientConfig, ClientIds } from './types'
const randomHash = () => ( const randomHash = () => (
(Math.random() ** Math.random()) * 9999999999999999 (Math.random() ** Math.random()) * 9999999999999999
@ -14,10 +14,11 @@ const params = {
export const facr: ClientConfig = { export const facr: ClientConfig = {
auth: { auth: {
clientId: 'facr-ott-web', clientId: ClientIds.Facr,
metaDataUrlParams: `?hash=${randomHash()}`, metaDataUrlParams: `?hash=${randomHash()}`,
}, },
defaultLanguage: 'cs', defaultLanguage: 'cs',
description: 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices',
disabledPreferences: true, disabledPreferences: true,
requests: { requests: {
[PROCEDURES.get_matches]: params, [PROCEDURES.get_matches]: params,
@ -45,4 +46,5 @@ export const facr: ClientConfig = {
userAccountLogoHeight: 3, userAccountLogoHeight: 3,
userAccountLogoWidth: 3.5, userAccountLogoWidth: 3.5,
}, },
title: 'FACR.TV - The home of Czech football streaming',
} }

@ -1,9 +1,10 @@
import type { ClientConfig } from './types' import { ClientConfig, ClientIds } from './types'
export const instat: ClientConfig = { export const instat: ClientConfig = {
auth: { auth: {
clientId: 'ott-web', clientId: ClientIds.Instat,
}, },
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
showSearch: true, showSearch: true,
styles: { styles: {
background: 'background-image: url(/images/Checker.png);', background: 'background-image: url(/images/Checker.png);',
@ -16,4 +17,5 @@ export const instat: ClientConfig = {
userAccountLogoHeight: 1.465, userAccountLogoHeight: 1.465,
userAccountLogoWidth: 6.37, userAccountLogoWidth: 6.37,
}, },
title: 'InStat TV - The Home of Sports Streaming',
} }

@ -6,12 +6,18 @@ type ProcedureName = string
type RequestParameters = any type RequestParameters = any
type StyledCss = ReturnType<typeof css> type StyledCss = ReturnType<typeof css>
export enum ClientIds {
Facr = 'facr-ott-web',
Instat = 'ott-web',
}
export type ClientConfig = { export type ClientConfig = {
auth: { auth: {
clientId: string, clientId: ClientIds,
metaDataUrlParams?: string, metaDataUrlParams?: string,
}, },
defaultLanguage?: Languages, defaultLanguage?: Languages,
description: string,
disabledPreferences?: boolean, disabledPreferences?: boolean,
requests?: Record<ProcedureName, RequestParameters>, requests?: Record<ProcedureName, RequestParameters>,
showPoweredByLogo?: boolean, showPoweredByLogo?: boolean,
@ -28,4 +34,5 @@ export type ClientConfig = {
userAccountLogoHeight?: number, userAccountLogoHeight?: number,
userAccountLogoWidth?: number, userAccountLogoWidth?: number,
}, },
title: string,
} }

@ -1,4 +1,4 @@
import { includes } from 'lodash' import includes from 'lodash/includes'
export const isIphone = includes(window.navigator.userAgent, 'iPhone') export const isIphone = includes(window.navigator.userAgent, 'iPhone')

@ -1,7 +1,9 @@
import { Suspense } from 'react' import { Suspense } from 'react'
import { Router } from 'react-router-dom' import { Router } from 'react-router-dom'
import { history } from 'config' import { history } from 'config/history'
import { client } from 'config/clients'
import { setClientTitleAndDescription } from 'helpers/setClientHeads'
import { isMatchPage } from 'helpers/isMatchPage' import { isMatchPage } from 'helpers/isMatchPage'
@ -14,6 +16,8 @@ import { JoinMatchPage } from 'features/JoinMatchPage'
import { AuthenticatedApp } from './AuthenticatedApp' import { AuthenticatedApp } from './AuthenticatedApp'
setClientTitleAndDescription(client.title, client.description)
const Main = () => { const Main = () => {
const { loadingUser, user } = useAuthStore() const { loadingUser, user } = useAuthStore()
@ -27,7 +31,7 @@ const Main = () => {
return <AuthenticatedApp /> return <AuthenticatedApp />
} }
export const App = () => ( const OTTApp = () => (
<Router history={history}> <Router history={history}>
<Theme> <Theme>
<GlobalStyles /> <GlobalStyles />
@ -41,3 +45,5 @@ export const App = () => (
</Theme> </Theme>
</Router> </Router>
) )
export default OTTApp

@ -13,7 +13,7 @@ const rotate = keyframes`
} }
` `
export const Wrapper = styled.button<ArrowLoaderProps>` export const Wrapper = styled.div<ArrowLoaderProps>`
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
border-color: transparent; border-color: transparent;
width: ${({ width = 'auto' }) => width}; width: ${({ width = 'auto' }) => width};

@ -0,0 +1,51 @@
import { lazy } from 'react'
import {
Redirect,
Route,
Switch,
} from 'react-router-dom'
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { useLexicsConfig } from 'features/LexicsStore'
import { PAGES } from '../../config/pages'
import { lexics } from '../../config/lexics'
const Login = lazy(() => import('../Login'))
const Registration = lazy(() => import('../Registration'))
const Main = styled.main`
width: 100%;
${isMobileDevice
? css`
padding: 0 12px;
@media screen and (orientation: landscape){
min-height: 100vh;
}
`
: ''};
`
export const App = () => {
useLexicsConfig(lexics)
return (
<Main>
<Switch>
<Route path={PAGES.login}>
<Login />
</Route>
<Route path={PAGES.registration}>
<Registration />
</Route>
<Redirect to={PAGES.login} />
</Switch>
</Main>
)
}

@ -9,7 +9,10 @@ import {
} from './styled' } from './styled'
type Props = Pick<InputHTMLAttributes<HTMLInputElement>, ( type Props = Pick<InputHTMLAttributes<HTMLInputElement>, (
'autoComplete' |
'autoFocus' |
'className' | 'className' |
'name' |
'onBlur' | 'onBlur' |
'onChange' | 'onChange' |
'onFocus' | 'onFocus' |
@ -22,9 +25,12 @@ type Props = Pick<InputHTMLAttributes<HTMLInputElement>, (
rightContent?: ReactNode, rightContent?: ReactNode,
} }
export const NewInput = ({ export const Input = ({
autoComplete,
autoFocus,
className, className,
leftContent, leftContent,
name,
onBlur, onBlur,
onChange, onChange,
onFocus, onFocus,
@ -41,12 +47,15 @@ export const NewInput = ({
<Label> <Label>
{leftContent} {leftContent}
<InputStyled <InputStyled
autoComplete={autoComplete}
autoFocus={autoFocus}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
onFocus={onFocus} onFocus={onFocus}
placeholder={placeholder ?? translate(placeholderLexic)} placeholder={placeholder ?? translate(placeholderLexic)}
type={type} type={type}
value={value} value={value}
name={name}
/> />
{rightContent} {rightContent}
</Label> </Label>

@ -1,25 +1,22 @@
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { devices } from 'config' import { devices } from 'config/devices'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { client } from 'features/AuthServiceApp/config/clients'
export const Wrapper = styled.div` export const Wrapper = styled.div`
background-color: ${({ theme }) => theme.colors.inputs}; background-color: ${({ theme }) => theme.colors.inputs};
width: 100%; width: 100%;
height: 2.123rem; height: 45px;
${isMobileDevice
? css`
height: 30px;
min-height: 30px;
position: relative;
`
: ''};
:not(:last-child) { :not(:last-child) {
border-bottom: 0.5px solid ${({ theme }) => theme.colors.black40}; border-bottom: 0.5px solid ${({ theme }) => theme.colors.black40};
} }
${client.styles.input}
` `
export const Label = styled.label` export const Label = styled.label`
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
@ -30,10 +27,10 @@ export const Label = styled.label`
letter-spacing: -0.01em; letter-spacing: -0.01em;
${isMobileDevice ${isMobileDevice
? css` ? css`
font-size: 12px; font-size: 12px;
` `
: ''}; : ''};
@media ${devices.tablet} { @media ${devices.tablet} {
font-size: 1.6rem; font-size: 1.6rem;
} }
@ -59,30 +56,29 @@ const resetStyles = css`
export const InputStyled = styled.input` export const InputStyled = styled.input`
${resetStyles} ${resetStyles}
padding: 0 0.755rem; padding: 0 16px;
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;
color: ${({ theme }) => theme.colors.text100}; color: ${({ theme }) => theme.colors.text100};
font-size: 16px;
letter-spacing: 0.1px;
::placeholder { ::placeholder {
font-weight: normal; font-weight: normal;
width: 100%; width: 100%;
color: ${({ theme }) => theme.colors.text100}; color: ${({ theme }) => theme.colors.text50};
font-style: normal; font-style: normal;
font-size: 0.755rem;
letter-spacing: 0.1px; letter-spacing: 0.1px;
${isMobileDevice
? css`
font-size: 12.31px;
`
: ''}
} }
${isMobileDevice
${({ type }) => (type === 'password'
? css` ? css`
font-size: 12.31px; padding-right: 0;
padding: 0 30px 0 10px; font-weight: 700;
letter-spacing: 6px;
` `
: ''}; : css`
font-weight: normal;
font-weight: ${({ type }) => (type === 'password' ? 700 : 'normal')}; `
)};
` `

@ -0,0 +1,32 @@
import { useMemo } from 'react'
import find from 'lodash/find'
import { langsList, Languages } from 'config/languages'
import { useToggle } from 'hooks/useToggle'
import { useLexicsStore } from 'features/LexicsStore'
export const useLanguageSelect = () => {
const { changeLang, lang } = useLexicsStore()
const {
close,
isOpen,
open,
} = useToggle()
const handleLangChange = (locale: Languages) => () => {
changeLang(locale)
close()
}
const selectedLang = useMemo(() => find(langsList, { locale: lang }), [lang])
return {
close,
handleLangChange,
isOpen,
open,
selectedLang,
}
}

@ -0,0 +1,70 @@
import map from 'lodash/map'
import { langsList } from 'config/languages'
import { OutsideClick } from 'features/OutsideClick'
import { useLanguageSelect } from './hooks'
import {
Wrapper,
Button,
ButtonTitle,
ListWrapper,
LangsList,
LangsItem,
FlagIcon,
LangName,
} from './styled'
export const LanguageSelect = () => {
const {
close,
handleLangChange,
isOpen,
open,
selectedLang,
} = useLanguageSelect()
return (
<Wrapper>
{selectedLang && (
<Button
type='button'
aria-controls='langsList'
aria-expanded={isOpen}
onClick={open}
>
<FlagIcon flag={selectedLang.className} />
<ButtonTitle>{selectedLang.title}</ButtonTitle>
</Button>
)}
{isOpen && (
<OutsideClick onClick={close}>
<ListWrapper>
<LangsList id='langsList'>
{
map(
langsList,
({
className,
locale,
title,
}) => (
<LangsItem
key={locale}
selected={selectedLang?.locale === locale}
onClick={handleLangChange(locale)}
>
<FlagIcon flag={className} />
<LangName>{title}</LangName>
</LangsItem>
),
)
}
</LangsList>
</ListWrapper>
</OutsideClick>
)}
</Wrapper>
)
}

@ -0,0 +1,147 @@
import styled, { css } from 'styled-components/macro'
import { langsList } from 'config/languages'
import { customScrollbar } from 'features/Common/customScrollbar'
export const Wrapper = styled.div`
position: relative;
display: flex;
align-items: center;
`
export const Button = styled.button`
padding: 0;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
width: 192px;
height: 44px;
padding-left: 20px;
::after {
content: '';
position: absolute;
right: 2px;
bottom: 50%;
transform: rotate(45deg);
width: 9px;
height: 9px;
border-right: 2px solid;
border-bottom: 2px solid;
border-color: ${({ theme }) => theme.colors.text100};
}
`
export const ButtonTitle = styled.span`
padding-left: 10px;
font-size: 16px;
font-weight: normal;
letter-spacing: -0.32px;
color: ${({ theme }) => theme.colors.text100};
`
export const ListWrapper = styled.div`
width: 192px;
height: 220px;
z-index: 1;
position: absolute;
top: calc(100% - 4px);
left: 0;
padding: 10px;
background-color: #333333;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
`
export const LangsList = styled.ul`
display: flex;
height: 100%;
flex-wrap: wrap;
overflow-y: auto;
${customScrollbar}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
`
type LangsItemProps = {
selected: boolean,
}
export const LangsItem = styled.li<LangsItemProps>`
padding-left: 10px;
margin-right: 10px;
text-align: center;
display: flex;
align-items: center;
width: 100%;
height: 34px;
border-radius: 2px;
transition: 0.3s;
font-weight: normal;
font-size: 16px;
color: ${({ theme }) => theme.colors.text70};
:hover {
background-color: #99999940;
cursor: pointer;
}
${({ selected, theme }) => (
selected
? css`
font-weight: 600;
color: ${theme.colors.text100};
`
: ''
)}
`
export const LangName = styled.span`
padding-left: 10px;
`
type LanguageFlags = typeof langsList[number]['className']
type Position = {
col: number,
row: number,
}
const flagPositions: Record<LanguageFlags, Position> = {
cz: { col: 1, row: 3 },
de: { col: 5, row: 4 },
es: { col: 12, row: 10 },
fr: { col: 1, row: 4 },
gb: { col: 4, row: 12 },
it: { col: 8, row: 5 },
pl: { col: 4, row: 9 },
pt: { col: 5, row: 9 },
ru: { col: 8, row: 9 },
ua: { col: 2, row: 12 },
}
const getFlagPosition = (flag: LanguageFlags) => {
const { col, row } = flagPositions[flag]
return `${col * -24}px ${row * -16}px`
}
type FlagIconProps = {
flag: LanguageFlags,
}
export const FlagIcon = styled.span<FlagIconProps>`
display: inline-block;
width: 20px;
height: 14px;
background-image: url('/images/flags-sprite.png');
background-repeat: no-repeat;
background-size: 360px;
background-position: ${({ flag }) => getFlagPosition(flag)};
`

@ -0,0 +1,75 @@
import type { FormEvent } from 'react'
import {
useRef,
useState,
useEffect,
} from 'react'
import { addLanguageUrlParam } from 'helpers/languageUrlParam'
import { loginCheck } from 'features/AuthServiceApp/requests/auth'
import { getApiUrl } from 'features/AuthServiceApp/config/routes'
import { useLexicsStore } from 'features/LexicsStore'
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields'
const url = getApiUrl('/authorize')
export const useLoginForm = () => {
const { lang } = useLexicsStore()
const [authError, setAuthError] = useState('')
const [isFetching, setIsFetching] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const {
email,
error: formError,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
} = useAuthFields()
const isSubmitDisabled = (
!email
|| !password
|| Boolean(formError)
|| Boolean(authError)
|| isFetching
)
const submitForm = () => formRef.current?.submit()
const handleError = (error: string) => {
setAuthError(error)
setIsFetching(false)
}
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setIsFetching(true)
loginCheck(email, password)
.then(submitForm)
.catch(handleError)
}
useEffect(() => {
setAuthError('')
}, [email, password])
return {
authError,
email,
formError,
formRef,
handleSubmit,
isFetching,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
url: addLanguageUrlParam(lang, url),
}
}

@ -0,0 +1,96 @@
import { T9n } from 'features/T9n'
import { ArrowLoader } from 'features/ArrowLoader'
import { PAGES } from '../../config/pages'
import { LanguageSelect } from '../LanguageSelect'
import { PasswordInput } from '../PasswordInput'
import { Input } from '../Input'
import { Logo } from '../Logo'
import { useLoginForm } from './hooks'
import {
InputGroup,
BlockTitle,
CenterBlock,
ButtonsBlock,
Form,
ButtonSolid,
Error,
LanguageSelectWrapper,
} from '../../styled'
import { RegisterButton } from './styled'
const Login = () => {
const {
authError,
email,
formError,
formRef,
handleSubmit,
isFetching,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
url,
} = useLoginForm()
return (
<CenterBlock>
<Logo />
<Form
method='POST'
ref={formRef}
action={url}
onSubmit={handleSubmit}
>
<BlockTitle t='step_title_login' />
<InputGroup>
<Input
autoFocus
type='email'
name='email'
autoComplete='email'
placeholderLexic='form_email'
value={email}
onChange={onEmailChange}
onBlur={onEmailBlur}
/>
<PasswordInput
type='password'
name='password'
autoComplete='current-password'
placeholderLexic='form_password'
value={password}
onChange={onPasswordChange}
onBlur={onPasswordBlur}
/>
</InputGroup>
<ButtonsBlock>
<Error>
<T9n t={formError} />
<T9n t={authError} />
</Error>
<ButtonSolid type='submit' disabled={isSubmitDisabled}>
{
isFetching
? <ArrowLoader />
: <T9n t='login' />
}
</ButtonSolid>
<RegisterButton to={`${PAGES.registration}${window.location.search}`}>
<T9n t='register' />
</RegisterButton>
</ButtonsBlock>
<LanguageSelectWrapper>
<LanguageSelect />
</LanguageSelectWrapper>
</Form>
</CenterBlock>
)
}
export default Login

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom'
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { outlineButtonStyles } from 'features/Common/Button'
export const RegisterButton = styled(Link)`
${outlineButtonStyles}
width: 100%;
height: 50px;
border-radius: 5px;
margin-top: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
${isMobileDevice
? css`
height: 44px;
margin-top: 12px;
font-weight: 500;
font-size: 17px;
border-radius: 10px;
@media screen and (orientation: landscape) {
margin: 12px auto 0;
}
`
: ''};
`

@ -0,0 +1,27 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { client } from '../../config/clients'
type Props = {
height?: number,
width?: number,
}
export const Logo = styled.div<Props>`
${client.styles.logo};
display: block;
background-size: contain;
background-repeat: no-repeat;
${isMobileDevice
? css`
width: 207px;
height: 48px;
@media screen and (orientation: landscape){
width: 92px;
height: 22px;
}
`
: ''}
`

@ -2,46 +2,54 @@ import type { ComponentProps } from 'react'
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { devices } from 'config'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { useToggle } from 'hooks' import { useToggle } from 'hooks/useToggle'
import { NewInput } from 'features/Common' import { Input } from '../Input'
import { BaseButton } from 'features/PopupComponents'
const VisibilityButton = styled(BaseButton)` const VisibilityButton = styled.button`
width: 1.14rem; border: none;
height: 1.14rem; background: none;
background-image: url(/images/visibility.svg);
opacity: 0.4;
background-color: transparent; background-color: transparent;
border-radius: 0; padding: 0;
margin-right: 0.5rem;
display: flex;
cursor: pointer;
width: 50px;
height: 100%;
opacity: 0.4;
transition: background-color 0.3s;
:hover {
background-color: rgba(255, 255, 255, 0.22);
}
::after {
content: '';
width: 100%;
height: 100%;
background-image: url(/images/visibility.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 22px 15px;
}
${isMobileDevice ${isMobileDevice
? css` ? css`
min-width: 13.55px; position: unset;
height: 9.23px; width: 50px;
right: 5px; height: 100%;
top: 50%; `
transform: translateY(-50%);
@media (orientation: landscape){
right: 10px;
}
`
: ''}; : ''};
@media ${devices.tablet} {
width: 2.2rem;
height: 1.5rem;
margin-right: 1.7rem;
}
` `
type Props = ComponentProps<typeof NewInput> type Props = ComponentProps<typeof Input>
export const PasswordInput = ({ export const PasswordInput = ({
autoComplete,
className, className,
leftContent, leftContent,
name,
onBlur, onBlur,
onChange, onChange,
onFocus, onFocus,
@ -51,7 +59,8 @@ export const PasswordInput = ({
}: Props) => { }: Props) => {
const { isOpen: showPassword, toggle } = useToggle() const { isOpen: showPassword, toggle } = useToggle()
return ( return (
<NewInput <Input
autoComplete={autoComplete}
className={className} className={className}
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
@ -59,6 +68,7 @@ export const PasswordInput = ({
placeholder={placeholder} placeholder={placeholder}
placeholderLexic={placeholderLexic} placeholderLexic={placeholderLexic}
value={value} value={value}
name={name}
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
leftContent={leftContent} leftContent={leftContent}
rightContent={<VisibilityButton type='button' onClick={toggle} />} rightContent={<VisibilityButton type='button' onClick={toggle} />}

@ -0,0 +1,63 @@
import { T9n } from 'features/T9n'
import {
Modal,
Wrapper,
Header,
HeaderTitle,
Body,
Footer,
ApplyButton,
// SendConfirmationButton,
Text,
} from './styled'
type Props = {
email: string,
handleModalClose: () => void,
isModalOpen: boolean,
}
export const RegisterPopup = (props: Props) => {
const {
email,
handleModalClose,
isModalOpen,
} = props
// const handleNewConfirmation = () => {
// // TODO дописать логику для отправки доп. письма, может понадобится, когда допишут бэк
// // console.log('send new confirmation')
// }
return (
<Modal isOpen={isModalOpen} withCloseButton={false}>
<Wrapper>
<Header>
<HeaderTitle>
<T9n t='confirm_email' />
</HeaderTitle>
</Header>
<Body>
<Text>
<T9n t='to_email' />&nbsp;
{email}&nbsp;
<T9n t='send_confirm' />&nbsp;
</Text>
<Text>
<T9n t='confirm_2_hours' />
</Text>
<Text>
<T9n t='check_email' />
</Text>
</Body>
<Footer>
<ApplyButton onClick={() => handleModalClose()}>Ok</ApplyButton>
{/* <SendConfirmationButton onClick={handleNewConfirmation}>
<T9n t='send_new_email' />
</SendConfirmationButton> */}
</Footer>
</Wrapper>
</Modal>
)
}

@ -0,0 +1,164 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
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 { ButtonSolid, ButtonOutline } from 'features/Common'
export const Modal = styled(BaseModal)`
background-color: rgba(0, 0, 0, 0.7);
padding: 0 60px;
${ModalWindow} {
max-width: 757px;
min-height: 414px;
padding-top: 60px;
background-color: #333333;
border-radius: 5px;
@media (max-width: 1370px) {
width: 70rem;
height: auto;
}
${isMobileDevice
? css`
@media ${devices.mobile}{
height: auto;
top: -7vh;
}
`
: ''};
}
`
type WrapperProps = {
isFetching?: boolean,
}
export const Wrapper = styled.div<WrapperProps>`
${({ isFetching }) => (
isFetching
? css`pointer-events: none;`
: ''
)}
`
export const Header = styled(BaseHeader)`
height: auto;
padding-top: 60;
justify-content: center;
${isMobileDevice
? css`
@media ${devices.mobile}{
padding-top: 33px;
}
`
: ''};
`
export const HeaderTitle = styled.span`
font-weight: 700;
font-size: 24px;
line-height: 24px;
color: #FFFFFF;
${isMobileDevice
? css`
@media ${devices.mobile}{
font-size: 14px;
line-height: 20px;
}
@media (orientation: landscape) {
font-size: 20px;
}
`
: ''};
`
export const Body = styled.div`
padding: 25px 94px 0 78px;
display: flex;
flex-direction: column;
font-weight: normal;
font-size: 20px;
line-height: 27px;
${isMobileDevice
? css`
@media ${devices.mobile}{
padding: 13px 25px 0;
flex-direction: column;
}
@media (orientation: landscape){
padding: 22px 23px 0 29px;
}
`
: ''};
`
export const Footer = styled.div`
width: 100%;
display: flex;
justify-content: center;
padding: 1.89rem;
${isMobileDevice
? css`
@media ${devices.mobile}{
flex-direction: column;
padding: 20px 25px 20px;
}
`
: ''};
`
export const ApplyButton = styled(ButtonSolid)`
width: 270px;
border-radius: 5px;
font-weight: 600;
margin-right: 24px;
width: 134px;
height: 50px;
font-size: 20px;
${isMobileDevice
? css`
@media ${devices.mobile}{
width: 100%;
min-height: 42px;
margin-bottom: 20px;
}
@media (orientation: landscape){
width: 290px;
min-height: 42px;
}
`
: ''};
`
export const SendConfirmationButton = styled(ButtonOutline)`
width: 100%;
height: 50px;
border-radius: 5px;
font-weight: 500;
font-size: 20px;
${isMobileDevice
? css`
@media ${devices.mobile}{
width: 100%;
}
@media (orientation: landscape){
width: 290px;
}
`
: ''};
`
export const Text = styled.span`
margin-bottom: 20px;
`

@ -0,0 +1,72 @@
import { SyntheticEvent, useState } from 'react'
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields'
import { registerCheck } from '../../requests/register'
export const useRegistrationForm = () => {
const [authError, setAuthError] = useState('')
const [termsAccepted, setTermsAccepted] = useState(false)
const [isFetching, setIsFetching] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const {
email,
error: formError,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
} = useAuthFields()
const isSubmitDisabled = (
!email
|| !password
|| !termsAccepted
|| Boolean(formError)
|| isFetching
)
const handleSubmit = async (event: SyntheticEvent) => {
event?.preventDefault()
setIsFetching(true)
try {
await registerCheck({
email,
password,
})
setAuthError('')
setIsFetching(false)
setIsModalOpen(true)
} catch (err) {
setAuthError(String(err))
setIsFetching(false)
}
}
const handleModalClose = () => {
setIsModalOpen(false)
}
const onTermsChange = () => {
setTermsAccepted(!termsAccepted)
}
return {
authError,
email,
formError,
handleModalClose,
handleSubmit,
isFetching,
isModalOpen,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
onTermsChange,
password,
termsAccepted,
}
}

@ -0,0 +1,125 @@
import { useHistory } from 'react-router'
import { T9n } from 'features/T9n'
import { Checkbox } from 'features/Common/Checkbox'
import { ArrowLoader } from 'features/ArrowLoader'
import { RegisterPopup } from 'features/AuthServiceApp/components/RegisterPopup'
import { LanguageSelect } from '../LanguageSelect'
import { PasswordInput } from '../PasswordInput'
import { Input } from '../Input'
import { Logo } from '../Logo'
import { useRegistrationForm } from './hooks'
import {
BlockTitle,
CenterBlock,
InputGroup,
ButtonsBlock,
Form,
ButtonSolid,
Error,
LanguageSelectWrapper,
} from '../../styled'
import {
Label,
Link,
ButtonOutline,
CheckboxWrapper,
} from './styled'
const Registration = () => {
const history = useHistory()
const {
authError,
email,
formError,
handleModalClose,
handleSubmit,
isFetching,
isModalOpen,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
onTermsChange,
password,
termsAccepted,
} = useRegistrationForm()
return (
<CenterBlock>
<Logo />
<Form onSubmit={handleSubmit}>
<BlockTitle t='step_title_registration' />
<InputGroup>
<Input
type='email'
name='email'
autoComplete='email'
placeholderLexic='form_email'
value={email}
onChange={onEmailChange}
onBlur={onEmailBlur}
/>
<PasswordInput
type='password'
name='password'
autoComplete='current-password'
placeholderLexic='form_password'
value={password}
onChange={onPasswordChange}
onBlur={onPasswordBlur}
/>
</InputGroup>
<Error>
{formError
? <T9n t={formError} />
: <T9n t={authError} />}
</Error>
<CheckboxWrapper>
<Checkbox
checked={termsAccepted}
onChange={onTermsChange}
label={(
<Label>
<T9n t='i_accept' />
<Link href='/terms-and-conditions.html' target='_blank'>
<T9n t='terms_and_conditions' />
</Link>
</Label>
)}
/>
</CheckboxWrapper>
<ButtonsBlock>
<ButtonSolid disabled={isSubmitDisabled} type='submit'>
{
isFetching
? <ArrowLoader />
: <T9n t='sign_up' />
}
</ButtonSolid>
<ButtonOutline
type='button'
onClick={history.goBack}
>
<T9n t='go_back' />
</ButtonOutline>
</ButtonsBlock>
<LanguageSelectWrapper>
<LanguageSelect />
</LanguageSelectWrapper>
<RegisterPopup
email={email}
isModalOpen={isModalOpen}
handleModalClose={handleModalClose}
/>
</Form>
</CenterBlock>
)
}
export default Registration

@ -0,0 +1,43 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { ButtonOutline as ButtonOutlineBase } from 'features/Common/Button'
export const ButtonOutline = styled(ButtonOutlineBase)`
width: 100%;
height: 50px;
margin-top: 15px;
border-radius: 5px;
font-weight: normal;
font-size: 20px;
${isMobileDevice
? css`
display: block;
height: 44px;
margin-top: 12px;
font-weight: 600;
font-size: 17px;
border-radius: 10px;
`
: ''};
`
export const CheckboxWrapper = styled.div`
width: 100%;
display: flex;
margin-top: 4px;
margin-bottom: 24px;
`
export const Label = styled.span`
font-weight: normal;
font-size: 14px;
line-height: 21px;
`
export const Link = styled.a`
color: ${({ theme }) => theme.colors.text100};
text-decoration: underline;
margin-left: 6px;
`

@ -0,0 +1,40 @@
import styled, { css } from 'styled-components/macro'
import { facr as platformFacr } from 'config/clients/facr'
import type { ClientConfig } from './types'
const Background = styled.div`
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
background-color: #00257A;
`
export const facr: ClientConfig = {
...platformFacr,
background: Background,
name: 'facr',
styles: {
input: css`
background-color: transparent;
:not(:last-child) {
border-color: ${({ theme }) => theme.colors.text100};
}
`,
inputGroup: css`
border: 1px solid ${({ theme }) => theme.colors.text100};
`,
logo: css`
background-image: url(/images/facr_auth_logo.png);
height: 163px;
width: 213px;
`,
submitButton: css`
background-color: ${({ theme }) => theme.colors.text100};
color: #00257A;
`,
},
}

@ -0,0 +1,13 @@
import { ClientIds } from 'config/clients/types'
import { facr } from './facr'
import { instat } from './instat'
const clients = {
[ClientIds.Facr]: facr,
[ClientIds.Instat]: instat,
}
const params = new URLSearchParams(window.location.search)
const clientId = params.get('client_id') as ClientIds
export const client = clients[clientId] || instat

@ -0,0 +1,20 @@
import { css } from 'styled-components'
import { instat as platformInstat } from 'config/clients/instat'
import { Background } from 'features/Background'
import type { ClientConfig } from './types'
export const instat: ClientConfig = {
...platformInstat,
background: Background,
name: 'instat',
styles: {
logo: css`
background-image: url(/images/logo.svg);
height: 54px;
width: 234px;
`,
},
}

@ -0,0 +1,20 @@
import type { ReactNode, FC } from 'react'
import { css } from 'styled-components/macro'
import type { Languages } from 'config/languages'
type StyledCss = ReturnType<typeof css>
export type ClientConfig = {
background: FC<{ children: ReactNode }>,
defaultLanguage?: Languages,
description: string,
name: string,
styles: {
input?: StyledCss,
inputGroup?: StyledCss,
logo: StyledCss,
submitButton?: StyledCss,
},
title: string,
}

@ -0,0 +1,32 @@
export const lexics = {
check_email: 15907,
check_password: 15842,
confirm_2_hours: 15906,
confirm_email: 15432,
error_email_already_in_use: 11156,
error_empty_email: 2498,
error_empty_password: 2499,
error_failed_to_send_email: 15902,
error_invalid_email_format: 12908,
error_invalid_email_or_password: 15774,
error_invalid_platform: 15925,
error_missing_required_argument: 15921,
error_simple_password: 12940,
error_unsupported_response_type: 15922,
error_user_already_created: 15926,
error_user_not_found: 1417,
form_email: 12912,
form_password: 751,
go_back: 1907,
i_accept: 15737,
login: 13404,
register: 13328,
registration_successful: 12945,
send_confirm: 15905,
send_new_email: 15830,
sign_up: 1305,
step_title_login: 13404,
step_title_registration: 1306,
terms_and_conditions: 15738,
to_email: 15904,
}

@ -0,0 +1,4 @@
export const PAGES = {
login: '/authorize',
registration: '/registration',
}

@ -0,0 +1,13 @@
import { ENV } from 'config/env'
const APIS = {
preproduction: '',
production: 'https://auth.instat.tv',
staging: 'https://test-auth.instat.tv',
}
export const API_ROOT = APIS[ENV]
export const getApiUrl = (path: string) => (
`${API_ROOT}${path}${window.location.search}`
)

@ -0,0 +1,22 @@
import { isValidEmail } from '..'
it('invalid emails', () => {
expect(isValidEmail('a')).toBeFalsy()
expect(isValidEmail('1')).toBeFalsy()
expect(isValidEmail('a@')).toBeFalsy()
expect(isValidEmail('a@m')).toBeFalsy()
expect(isValidEmail('a@m.')).toBeFalsy()
expect(isValidEmail('a@mail')).toBeFalsy()
expect(isValidEmail('a@mail.')).toBeFalsy()
expect(isValidEmail('abcd.mail.com')).toBeFalsy()
})
it('valid emails', () => {
expect(isValidEmail('a@mail.com')).toBeTruthy()
expect(isValidEmail('A@MAIL.COM')).toBeTruthy()
expect(isValidEmail('123@mail.com')).toBeTruthy()
expect(isValidEmail('A123@mail.com')).toBeTruthy()
expect(isValidEmail('a.b@mail.com')).toBeTruthy()
expect(isValidEmail('a-b-c@mail.com')).toBeTruthy()
expect(isValidEmail('a-b@a-b.com')).toBeTruthy()
})

@ -0,0 +1,9 @@
import size from 'lodash/size'
export const emailRegex = '[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?'
const emailRegExp = new RegExp(emailRegex)
export const isValidEmail = (email: string) => (
emailRegExp.test(email.toLowerCase()) && size(email) <= 100
)

@ -0,0 +1,58 @@
import type {
ChangeEvent,
FocusEvent,
} from 'react'
import { useState } from 'react'
import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail'
import { isValidPassword } from 'features/AuthServiceApp/helpers/isValidPassword'
export const useAuthFields = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const checkPassword = (pass: string) => {
const regexp = /^(?=[^.,'"-])[\w\d@$!%*#?&;*]{8,}$/g
return regexp.test(pass)
}
const onEmailChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
setError('')
setEmail(value)
}
const onPasswordChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
setError('')
setPassword(value)
if (!checkPassword(value)) {
setError('check_password')
}
}
const onEmailBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => {
if (!value) {
setError('error_empty_email')
} else if (!isValidEmail(value)) {
setError('error_invalid_email_format')
}
}
const onPasswordBlur = ({ target: { value } }: FocusEvent<HTMLInputElement>) => {
if (!value) {
setError('error_empty_password')
} else if (!isValidPassword(value)) {
setError('error_simple_password')
}
}
return {
email,
error,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
}
}

@ -0,0 +1,35 @@
import { BrowserRouter } from 'react-router-dom'
import { getLanguageUrlParam } from 'helpers/languageUrlParam'
import {
setClientTitleAndDescription,
setClientIcons,
} from 'helpers/setClientHeads'
import { GlobalStyles } from 'features/GlobalStyles'
import { LexicsStore } from 'features/LexicsStore'
import { Theme } from 'features/Theme'
import { App } from './components/App'
import { client } from './config/clients'
setClientTitleAndDescription(client.title, client.description)
setClientIcons(client.name)
const Background = client.background
const initialLanguage = getLanguageUrlParam() || client.defaultLanguage
const AuthServiceApp = () => (
<BrowserRouter>
<Theme>
<GlobalStyles />
<LexicsStore initialLanguage={initialLanguage}>
<Background>
<App />
</Background>
</LexicsStore>
</Theme>
</BrowserRouter>
)
export default AuthServiceApp

@ -0,0 +1,36 @@
import { getApiUrl } from '../config/routes'
const errorLexics = {
1: 'error_invalid_email_or_password',
4: 'error_user_not_found',
}
type FailedResponse = {
error: {
code: keyof typeof errorLexics,
message?: string,
},
ok: false,
}
type SuccessResponse = {
ok: true,
}
export const loginCheck = async (email: string, password: string) => {
const url = getApiUrl('/authorize-check')
const init: RequestInit = {
body: new URLSearchParams({
email,
password,
}),
method: 'POST',
}
const response = await fetch(url, init)
const body: SuccessResponse | FailedResponse = await response.json()
if (body.ok) return Promise.resolve()
return Promise.reject(errorLexics[body.error.code])
}

@ -0,0 +1,49 @@
import { getApiUrl } from 'features/AuthServiceApp/config/routes'
const errorLexics = {
1: 'error_invalid_email_or_password',
2: 'error_missing_required_argument',
3: 'error_unsupported_response_type',
7: 'error_invalid_platform',
8: 'error_failed_to_send_email',
9: 'error_user_already_created',
}
type FailedResponse = {
error: {
code: keyof typeof errorLexics,
message?: string,
},
ok: false,
}
type SuccessResponse = {
ok: true,
}
type RegisterProps = {
email: string,
password: string,
}
export const registerCheck = async ({
email,
password,
} : RegisterProps) => {
const url = getApiUrl('/registration')
const init: RequestInit = {
body: new URLSearchParams({
email,
password,
}),
method: 'POST',
}
const response = await fetch(url, init)
const body: SuccessResponse | FailedResponse = await response.json()
if (body.ok) return Promise.resolve()
return Promise.reject(errorLexics[body.error.code])
}

@ -0,0 +1,125 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { ButtonSolid as BaseButtonSolid } from 'features/Common/Button'
import { T9n } from 'features/T9n'
import { client } from 'features/AuthServiceApp/config/clients'
export const InputGroup = styled.div`
width: 100%;
box-shadow: ${({ theme }) => theme.colors.shadow};
border-radius: 14px;
overflow: hidden;
${client.styles.inputGroup}
`
export const CenterBlock = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
margin-top: 9.15rem;
${isMobileDevice
? css`
margin-top: 107px;
@media screen and (orientation: landscape) {
width: 290px;
margin: auto;
}
`
: ''};
`
export const Form = styled.form`
width: 100%;
max-width: 493px;
margin-top: 1.82rem;
margin-bottom: 6.604rem;
display: flex;
flex-direction: column;
align-items: center;
${isMobileDevice
? css`
width: 100%;
margin-top: 78px;
@media screen and (orientation: landscape){
margin-bottom: 0;
margin-top: 10px;
padding: 0;
}
`
: ''};
`
export const BlockTitle = styled(T9n)`
display: block;
font-style: normal;
font-weight: bold;
font-size: 24px;
height: 24px;
color: ${({ theme: { colors } }) => colors.text100};
margin-bottom: 30px;
${isMobileDevice
? css`
font-size: 20px;
margin-bottom: 12px;
`
: ''};
`
type ButtonsBlockProps = {
marginTop?: number,
}
export const ButtonsBlock = styled.div<ButtonsBlockProps>`
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: ${({ marginTop = 0 }) => marginTop}px;
`
type ButtonSolidProps = {
width?: string,
}
export const ButtonSolid = styled(BaseButtonSolid)<ButtonSolidProps>`
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
font-weight: 600;
font-size: 20px;
${isMobileDevice
? css`
height: 44px;
font-weight: 600;
font-size: 17px;
border-radius: 10px;
`
: ''};
${client.styles.submitButton}
`
export const Error = styled.span`
align-self: flex-start;
height: 20px;
margin-left: 2px;
color: ${({ theme }) => theme.colors.error};
font-style: normal;
font-weight: 500;
font-size: 12px;
line-height: 20px;
letter-spacing: 0.1px;
`
export const LanguageSelectWrapper = styled.div`
margin-top: 15px;
align-self: flex-end;
`

@ -13,17 +13,20 @@ import isString from 'lodash/isString'
import { PAGES } from 'config' import { PAGES } from 'config'
import { addLanguageUrlParam } from 'helpers/languageUrlParam'
import { writeToken, removeToken } from 'helpers/token' import { writeToken, removeToken } from 'helpers/token'
import { setCookie, removeCookie } from 'helpers/cookie' import { setCookie, removeCookie } from 'helpers/cookie'
import { isMatchPage } from 'helpers/isMatchPage' import { isMatchPage } from 'helpers/isMatchPage'
import { useLocalStore, useToggle } from 'hooks' import { useLocalStore, useToggle } from 'hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { queryParamStorage } from 'features/QueryParamsStorage' import { queryParamStorage } from 'features/QueryParamsStorage'
import { getClientSettings } from '../helpers' import { getClientSettings } from '../helpers'
export const useAuth = () => { export const useAuth = () => {
const { lang } = useLexicsStore()
const history = useHistory() const history = useHistory()
const { const {
close: markUserLoaded, close: markUserLoaded,
@ -33,17 +36,18 @@ export const useAuth = () => {
const userManager = useMemo(() => new UserManager(getClientSettings()), []) const userManager = useMemo(() => new UserManager(getClientSettings()), [])
const login = useCallback(async () => ( const login = useCallback(async () => (
userManager.signinRedirect() userManager.signinRedirect({ extraQueryParams: { lang } })
), [userManager]) ), [userManager, lang])
const logout = useCallback(() => { const logout = useCallback(() => {
userManager.clearStaleState() userManager.clearStaleState()
userManager.createSigninRequest().then(({ url }) => { userManager.createSigninRequest().then(({ url }) => {
userManager.signoutRedirect({ post_logout_redirect_uri: url }) const urlWithLang = addLanguageUrlParam(lang, url)
userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang })
}) })
removeToken() removeToken()
removeCookie('access_token') removeCookie('access_token')
}, [userManager]) }, [userManager, lang])
const storeUser = useCallback((loadedUser: User) => { const storeUser = useCallback((loadedUser: User) => {
setUser(loadedUser) setUser(loadedUser)

@ -1,14 +0,0 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
export const InputGroup = styled.div`
width: 100%;
box-shadow: ${({ theme }) => theme.colors.shadow};
border-radius: 14px;
overflow: hidden;
${isMobileDevice
? css`
border-radius: 8px;
`
: ''};
`

@ -1,7 +1,4 @@
export * from './Input' export * from './Input'
export * from './NewInput'
export * from './PasswordInput'
export * from './InputGroup'
export * from './Button' export * from './Button'
export * from './Radio' export * from './Radio'
export * from './Checkbox' export * from './Checkbox'

@ -1,16 +1,21 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { client } from 'config/clients'
import { getLanguageUrlParam } from 'helpers/languageUrlParam'
import { AuthStore } from 'features/AuthStore' import { AuthStore } from 'features/AuthStore'
import { LexicsStore } from 'features/LexicsStore' import { LexicsStore } from 'features/LexicsStore'
const initialLanguage = getLanguageUrlParam() || client.defaultLanguage
type Props = { type Props = {
children: ReactNode, children: ReactNode,
} }
export const GlobalStores = ({ children }: Props) => ( export const GlobalStores = ({ children }: Props) => (
<AuthStore> <LexicsStore initialLanguage={initialLanguage}>
<LexicsStore> <AuthStore>
{children} {children}
</LexicsStore> </AuthStore>
</AuthStore> </LexicsStore>
) )

@ -7,6 +7,7 @@ export const GlobalStyles = createGlobalStyle`
html { html {
font-size: calc(2px + 1vw); font-size: calc(2px + 1vw);
overflow-y: hidden;
} }
body { body {

@ -4,6 +4,8 @@ import isEmpty from 'lodash/isEmpty'
import { getLexics } from 'requests' import { getLexics } from 'requests'
import type { Languages } from 'config/languages'
import { import {
getLexicIds, getLexicIds,
mapTranslationsToLocalKeys, mapTranslationsToLocalKeys,
@ -14,8 +16,8 @@ import { useLang } from './useLang'
import { useLexicsConfig } from './useLexicsConfig' import { useLexicsConfig } from './useLexicsConfig'
import { useTranslations } from './useTranslations' import { useTranslations } from './useTranslations'
export const useLexics = () => { export const useLexics = (initialLanguage?: Languages) => {
const { changeLang, lang } = useLang() const { changeLang, lang } = useLang(initialLanguage)
const { addLexicsConfig, lexicsConfig } = useLexicsConfig() const { addLexicsConfig, lexicsConfig } = useLexicsConfig()
const { addTranslations, translate } = useTranslations() const { addTranslations, translate } = useTranslations()

@ -3,16 +3,15 @@ import { useCallback } from 'react'
import { useLocalStore } from 'hooks' import { useLocalStore } from 'hooks'
import type { Languages } from 'config/languages' import type { Languages } from 'config/languages'
import { client } from 'config/clients'
import { isSupportedLang } from '../helpers/isSupportedLang' import { isSupportedLang } from '../helpers/isSupportedLang'
const LANG_KEY = 'lang' const LANG_KEY = 'lang'
const DEFAULT_LANG = client.defaultLanguage || 'en' const DEFAULT_LANG = 'en'
export const useLang = () => { export const useLang = (initialLanguage: Languages = DEFAULT_LANG) => {
const [lang, setLang] = useLocalStore<Languages>({ const [lang, setLang] = useLocalStore<Languages>({
defaultValue: DEFAULT_LANG, initialValue: initialLanguage,
key: LANG_KEY, key: LANG_KEY,
validator: isSupportedLang, validator: isSupportedLang,
}) })

@ -5,16 +5,21 @@ import {
useEffect, useEffect,
} from 'react' } from 'react'
import type { Languages } from 'config/languages'
import type { LexicsConfig, LexicsId } from './types' import type { LexicsConfig, LexicsId } from './types'
import { useLexics } from './hooks' import { useLexics } from './hooks'
type Context = ReturnType<typeof useLexics> type Context = ReturnType<typeof useLexics>
type Props = { children: ReactNode } type Props = {
children: ReactNode,
initialLanguage?: Languages,
}
const LexicsContext = createContext({} as Context) const LexicsContext = createContext({} as Context)
export const LexicsStore = ({ children }: Props) => { export const LexicsStore = ({ children, initialLanguage }: Props) => {
const lexics = useLexics() const lexics = useLexics(initialLanguage)
return <LexicsContext.Provider value={lexics}>{children}</LexicsContext.Provider> return <LexicsContext.Provider value={lexics}>{children}</LexicsContext.Provider>
} }

@ -0,0 +1,14 @@
import type { Languages } from 'config/languages'
import { history } from 'config/history'
const KEY = 'lang'
export const getLanguageUrlParam = () => (
new URLSearchParams(history.location.search).get(KEY) as Languages
)
export const addLanguageUrlParam = (lang: Languages, url: string) => {
const urlObject = new URL(url)
urlObject.searchParams.set(KEY, lang)
return urlObject.toString()
}

@ -0,0 +1,38 @@
export const setClientTitleAndDescription = (title: string, desc: string) => {
document.title = title
const description = document.querySelector('meta[name=description]') as HTMLMetaElement
description.content = desc
}
export const setClientIcons = (clientName: string) => {
const faviconLink = document.createElement('link')
faviconLink.rel = 'icon'
faviconLink.href = `/clients/${clientName}/favicon/favicon.ico`
const faviconAppleLink = document.createElement('link')
faviconLink.rel = 'apple-touch-icon'
faviconLink.type = 'image/png'
faviconLink.href = `/clients/${clientName}/favicon/apple-touch-icon.png`
const favicon32Link = document.createElement('link')
favicon32Link.rel = 'icon'
favicon32Link.type = 'image/png'
favicon32Link.href = `/clients/${clientName}/favicon/favicon-32x32.png`
const favicon16Link = document.createElement('link')
favicon16Link.rel = 'icon'
favicon16Link.type = 'image/png'
favicon16Link.href = `/clients/${clientName}/favicon/favicon-16x16.png`
const manifest = document.createElement('link')
manifest.rel = 'icon'
manifest.href = `/clients/${clientName}/favicon/manifest.json`
const { head } = window.document
head.appendChild(faviconLink)
head.appendChild(faviconAppleLink)
head.appendChild(favicon32Link)
head.appendChild(favicon16Link)
head.appendChild(manifest)
}

@ -9,7 +9,8 @@ const defaultSerializer = (key: string, value: any) => value
type Args<T> = { type Args<T> = {
clearOnUnmount?: boolean, clearOnUnmount?: boolean,
defaultValue: T, defaultValue?: T,
initialValue?: T,
key: string, key: string,
serialize?: (key: string, value: T) => string, serialize?: (key: string, value: T) => string,
@ -32,6 +33,7 @@ const createHook = (storage: Storage) => (
<T extends any>({ <T extends any>({
clearOnUnmount, clearOnUnmount,
defaultValue, defaultValue,
initialValue,
key, key,
serialize = defaultSerializer, serialize = defaultSerializer,
validator = defaultValidator, validator = defaultValidator,
@ -42,7 +44,7 @@ const createHook = (storage: Storage) => (
return isValid ? storeValue : defaultValue return isValid ? storeValue : defaultValue
} }
const [state, setState] = useState<T>(getInitialState) const [state, setState] = useState<T>(initialValue || getInitialState)
useEffect(() => { useEffect(() => {
const storeValue = readStorageInitialValue(storage, key) const storeValue = readStorageInitialValue(storage, key)

@ -1,13 +1,21 @@
import { StrictMode } from 'react' import {
lazy,
Suspense,
StrictMode,
} from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { App } from 'features/App'
import * as serviceWorker from './serviceWorker' import * as serviceWorker from './serviceWorker'
export const App = process.env.REACT_APP_TYPE === 'auth-service'
? lazy(() => import('features/AuthServiceApp'))
: lazy(() => import('features/App'))
ReactDOM.render( ReactDOM.render(
<StrictMode> <StrictMode>
<App /> <Suspense fallback={null}>
<App />
</Suspense>
</StrictMode>, </StrictMode>,
document.getElementById('root'), document.getElementById('root'),
) )

@ -5,5 +5,6 @@ declare namespace NodeJS {
export interface ProcessEnv { export interface ProcessEnv {
REACT_APP_CLIENT: 'instat' | 'facr', REACT_APP_CLIENT: 'instat' | 'facr',
REACT_APP_ENV: 'production' | 'preproduction' | 'staging', REACT_APP_ENV: 'production' | 'preproduction' | 'staging',
REACT_APP_TYPE: 'auth-service' | 'ott',
} }
} }

Loading…
Cancel
Save