diff --git a/Makefile b/Makefile
index 39f84f15..277e001a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ build:
production-build:
rm -rf build
- REACT_APP_PRODUCTION=true npm run build
+ REACT_APP_PRODUCTION=true REACT_APP_STRIPE_PK=pk_live_ANI76cBhSo69DZUxPmyRVIZW npm run build
.PHONY: build
diff --git a/package.json b/package.json
index 79bab867..f78ae89f 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,8 @@
"build-storybook": "build-storybook -s public"
},
"dependencies": {
+ "@stripe/react-stripe-js": "^1.4.0",
+ "@stripe/stripe-js": "^1.13.2",
"date-fns": "^2.14.0",
"history": "^4.10.1",
"hls.js": "^0.14.15",
diff --git a/public/index.html b/public/index.html
index eb0f51de..77ff5080 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,6 +7,7 @@
+
Instat TV
diff --git a/src/config/env.tsx b/src/config/env.tsx
index 7590618f..de85b24d 100644
--- a/src/config/env.tsx
+++ b/src/config/env.tsx
@@ -1 +1,3 @@
export const isProduction = process.env.REACT_APP_PRODUCTION === 'true'
+
+export const STRIPE_PUBLIC_KEY = process.env.REACT_APP_STRIPE_PK || 'pk_test_fkEjSoWfJXuCwMgwHRpbOGPt'
diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx
index a75b8e28..32f9cbef 100644
--- a/src/config/lexics/indexLexics.tsx
+++ b/src/config/lexics/indexLexics.tsx
@@ -1,4 +1,6 @@
+import { paymentLexics } from './payment'
import { proceduresLexics } from './procedures'
+import { publicLexics } from './public'
const matchPopupLexics = {
apply: 13491,
@@ -75,7 +77,6 @@ export const indexLexics = {
round_highilights: 13050,
save: 828,
search_results: 9014,
- select_language: 1005,
sport: 12993,
team: 658,
to_home: 13376,
@@ -88,4 +89,6 @@ export const indexLexics = {
...proceduresLexics,
...matchPopupLexics,
...buyMatchPopupLexics,
+ ...publicLexics,
+ ...paymentLexics,
}
diff --git a/src/config/lexics/payment.tsx b/src/config/lexics/payment.tsx
new file mode 100644
index 00000000..2f8315a2
--- /dev/null
+++ b/src/config/lexics/payment.tsx
@@ -0,0 +1,6 @@
+export const paymentLexics = {
+ add_card: 8313,
+ card_holder_name: 2021,
+ error_can_not_add_card: 14447,
+ error_payment_unsuccessful: 14446,
+}
diff --git a/src/config/lexics/userAccount.tsx b/src/config/lexics/userAccount.tsx
index 411a843a..cf6e031b 100644
--- a/src/config/lexics/userAccount.tsx
+++ b/src/config/lexics/userAccount.tsx
@@ -1,4 +1,5 @@
-import { publicLexics } from 'config/lexics/public'
+import { publicLexics } from './public'
+import { paymentLexics } from './payment'
const navigations = {
bank_card: 14205,
@@ -8,7 +9,6 @@ const navigations = {
}
export const userAccountLexics = {
- add_card: 8313,
change: 12614,
country: 835,
delete: 848,
@@ -27,4 +27,5 @@ export const userAccountLexics = {
user_account: 12928,
...navigations,
...publicLexics,
+ ...paymentLexics,
}
diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx
index 46377231..8fe91f0b 100644
--- a/src/config/procedures.tsx
+++ b/src/config/procedures.tsx
@@ -1,5 +1,6 @@
export const PROCEDURES = {
auth_user: 'auth_user',
+ bind_ott_subscription: 'bind_ott_subscription',
create_user: 'create_user',
get_cities: 'get_cities',
get_match_info: 'get_match_info',
diff --git a/src/features/AddCardForm/components/ElementContainer/index.tsx b/src/features/AddCardForm/components/ElementContainer/index.tsx
new file mode 100644
index 00000000..45be3d32
--- /dev/null
+++ b/src/features/AddCardForm/components/ElementContainer/index.tsx
@@ -0,0 +1,63 @@
+import { ReactNode } from 'react'
+
+import styled from 'styled-components/macro'
+
+import { T9n } from 'features/T9n'
+
+type LabelProps = {
+ backgroundColor?: string,
+ width?: string,
+}
+
+const Label = styled.label`
+ display: flex;
+ height: 50px;
+ width: ${({ width }) => width || '100%'};
+ padding: 0 25px;
+ background-color: ${({ backgroundColor = '#3F3F3F' }) => backgroundColor};
+ box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
+ border-radius: 2px;
+ border: 1px solid transparent;
+
+ font-family: Montserrat;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 16px;
+ line-height: 48px;
+ letter-spacing: -0.01em;
+ color: rgba(255, 255, 255, 0.5);
+
+ :not(:first-child) {
+ margin-top: 10px;
+ }
+`
+
+const Text = styled(T9n)`
+ margin-right: 20px;
+`
+
+const ElementWrapper = styled.div`
+ height: 100%;
+ flex-grow: 1;
+`
+
+type Props = {
+ backgroundColor?: string,
+ children: ReactNode,
+ label?: string,
+ width?: string,
+}
+
+export const ElementContainer = ({
+ backgroundColor,
+ children,
+ label,
+ width,
+}: Props) => (
+
+)
diff --git a/src/features/AddCardForm/components/Form/hooks/index.tsx b/src/features/AddCardForm/components/Form/hooks/index.tsx
new file mode 100644
index 00000000..23051384
--- /dev/null
+++ b/src/features/AddCardForm/components/Form/hooks/index.tsx
@@ -0,0 +1,55 @@
+import { FormEvent, useState } from 'react'
+
+import {
+ CardNumberElement,
+ useStripe,
+ useElements,
+} from '@stripe/react-stripe-js'
+
+import { useCardsStore } from 'features/CardsStore'
+
+export type Props = {
+ initialformOpen?: boolean,
+ inputsBackground?: string,
+ onAddSuccess?: () => void,
+ submitButton?: 'outline' |'solid',
+}
+
+export const useFormSubmit = ({ onAddSuccess }: Props) => {
+ const stripe = useStripe()
+ const elements = useElements()
+ const { onAddCard } = useCardsStore()
+ const [error, setError] = useState('')
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault()
+
+ if (!stripe || !elements) {
+ // Stripe.js has not loaded yet.
+ return
+ }
+
+ const name: string = e.currentTarget.cardHolderName.value
+
+ if (!name) {
+ setError('Name can not be empty')
+ return
+ }
+
+ const cardNumberElement = elements.getElement(CardNumberElement)
+ if (!cardNumberElement) return
+
+ const { error: tokenError, token } = await stripe.createToken(
+ cardNumberElement,
+ { name },
+ )
+
+ if (tokenError) {
+ setError(tokenError.message || '')
+ } else if (token) {
+ onAddCard(token.id).then(onAddSuccess)
+ }
+ }
+
+ return { error, handleSubmit }
+}
diff --git a/src/features/AddCardForm/components/Form/index.tsx b/src/features/AddCardForm/components/Form/index.tsx
new file mode 100644
index 00000000..243cb349
--- /dev/null
+++ b/src/features/AddCardForm/components/Form/index.tsx
@@ -0,0 +1,96 @@
+import {
+ CardNumberElement,
+ CardExpiryElement,
+ CardCvcElement,
+} from '@stripe/react-stripe-js'
+
+import { T9n } from 'features/T9n'
+import { useCardsStore } from 'features/CardsStore'
+import { OutlineButton, SolidButton } from 'features/UserAccount/styled'
+
+import { ElementContainer } from '../ElementContainer'
+
+import type { Props } from './hooks'
+import { useFormSubmit } from './hooks'
+
+import {
+ Form,
+ Column,
+ ButtonsBlock,
+ Input,
+ Errors,
+} from '../../styled'
+
+const baseStyles = {
+ color: '#fff',
+ fontFamily: 'Montserrat, Tahoma, sans-serif',
+ fontSize: '20px',
+ fontWeight: 'bold',
+ lineHeight: '50px',
+}
+
+const options = { placeholder: '', style: { base: baseStyles } }
+
+const buttons = {
+ outline: OutlineButton,
+ solid: SolidButton,
+}
+
+export const AddCardFormInner = (props: Props) => {
+ const {
+ inputsBackground,
+ submitButton = 'solid',
+ } = props
+ const { error: cardError } = useCardsStore()
+ const { error: formError, handleSubmit } = useFormSubmit(props)
+
+ const SubmitButton = buttons[submitButton]
+
+ return (
+
+ )
+}
diff --git a/src/features/AddCardForm/index.tsx b/src/features/AddCardForm/index.tsx
index 31440386..adef78fe 100644
--- a/src/features/AddCardForm/index.tsx
+++ b/src/features/AddCardForm/index.tsx
@@ -1,44 +1,42 @@
-import { SolidButton } from 'features/UserAccount/styled'
+import type { MouseEvent } from 'react'
-import {
- Form,
- Column,
- ButtonsBlock,
- Input,
-} from './styled'
+import { useToggle } from 'hooks'
-export const AddCardForm = () => (
-
-)
+import { T9n } from 'features/T9n'
+import { OutlineButton, Icon } from 'features/UserAccount/styled'
+
+import type { Props } from './components/Form/hooks'
+import { AddCardFormInner } from './components/Form'
+
+export const AddCardForm = ({
+ initialformOpen,
+ inputsBackground,
+ submitButton,
+}: Props) => {
+ const { isOpen, toggle } = useToggle(initialformOpen)
+
+ const onAddClick = (e: MouseEvent) => {
+ e.stopPropagation()
+ toggle()
+ }
+
+ return (
+ isOpen
+ ? (
+
+ )
+ : (
+
+
+
+
+ )
+ )
+}
diff --git a/src/features/AddCardForm/styled.tsx b/src/features/AddCardForm/styled.tsx
index 58d40c41..fb2d7560 100644
--- a/src/features/AddCardForm/styled.tsx
+++ b/src/features/AddCardForm/styled.tsx
@@ -1,8 +1,5 @@
import styled from 'styled-components/macro'
-import { Input as InputBase } from 'features/Common'
-import { InputWrapper } from 'features/Common/Input/styled'
-
export const Form = styled.form``
export const Column = styled.div`
@@ -14,21 +11,37 @@ export const Column = styled.div`
export const ButtonsBlock = styled.div`
display: flex;
+ flex-direction: column;
+ align-items: start;
margin-top: 40px;
`
-export const Input = styled(InputBase).attrs(() => ({
- withError: false,
-}))`
- width: auto;
+export const Input = styled.input`
+ color: #fff;
+ font-family: Montserrat, Tahoma, sans-serif;
+ font-size: 20px;
+ font-weight: bold;
+ line-height: 48px;
+ background-color: transparent;
+ border: none;
+ outline: none;
+ width: 100%;
+ padding: 0;
+ margin: 0;
- ${InputWrapper} {
- height: 50px;
- padding: 0 24px;
- margin-top: 10px;
+ :-webkit-autofill,
+ :-webkit-autofill:hover,
+ :-webkit-autofill:focus,
+ :-webkit-autofill:active {
+ box-shadow: 0 0 0 30px #535353 inset;
+ caret-color: ${({ theme: { colors } }) => colors.text};
+ -webkit-text-fill-color: ${({ theme: { colors } }) => colors.text};
}
+`
- :first-child ${InputWrapper} {
- margin-top: 0;
- }
+export const Errors = styled.span`
+ margin-bottom: 40px;
+ font-size: 16px;
+ line-height: 16px;
+ color: red;
`
diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx
index 27788ef0..962cf6ec 100644
--- a/src/features/App/AuthenticatedApp.tsx
+++ b/src/features/App/AuthenticatedApp.tsx
@@ -9,6 +9,8 @@ import {
import { indexLexics } from 'config/lexics/indexLexics'
import { PAGES } from 'config'
+import { StripeElements } from 'features/StripeElements'
+
import { useLexicsConfig } from 'features/LexicsStore'
import { ExtendedSearchStore, ExtendedSearchPage } from 'features/ExtendedSearchPage'
@@ -16,6 +18,7 @@ import { MatchSwitchesStore } from 'features/MatchSwitches'
import { UserFavoritesStore } from 'features/UserFavorites/store'
import { MatchPopup, MatchPopupStore } from 'features/MatchPopup'
import { BuyMatchPopup, BuyMatchPopupStore } from 'features/BuyMatchPopup'
+import { CardsStore } from 'features/CardsStore'
const HomePage = lazy(() => import('features/HomePage'))
const TeamPage = lazy(() => import('features/TeamPage'))
@@ -28,43 +31,47 @@ export const AuthenticatedApp = () => {
useLexicsConfig(indexLexics)
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
- {/* в Switch как прямой children можно рендерить только Route или Redirect */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {/* в Switch как прямой children можно рендерить только Route или Redirect */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/features/BuyMatchPopup/components/CardStep/index.tsx b/src/features/BuyMatchPopup/components/CardStep/index.tsx
new file mode 100644
index 00000000..f30f6eff
--- /dev/null
+++ b/src/features/BuyMatchPopup/components/CardStep/index.tsx
@@ -0,0 +1,56 @@
+import isEmpty from 'lodash/isEmpty'
+
+import { AddCardForm } from 'features/AddCardForm'
+import { useCardsStore } from 'features/CardsStore'
+import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
+import {
+ CloseButton,
+ Header,
+ HeaderActions,
+ HeaderTitle,
+} from 'features/PopupComponents'
+import { T9n } from 'features/T9n'
+
+import { CardsList } from '../CardsList'
+
+import {
+ Wrapper,
+ Body,
+ Footer,
+ Button,
+} from '../../styled'
+
+export const CardStep = () => {
+ const { cards } = useCardsStore()
+ const { close, subscribeToMatch } = useBuyMatchPopupStore()
+
+ const emptyCards = isEmpty(cards)
+
+ return (
+
+
+
+ {emptyCards ? 'Добавление карты' : 'Выберите карту для оплаты'}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/features/BuyMatchPopup/components/CardsList/index.tsx b/src/features/BuyMatchPopup/components/CardsList/index.tsx
new file mode 100644
index 00000000..79e698af
--- /dev/null
+++ b/src/features/BuyMatchPopup/components/CardsList/index.tsx
@@ -0,0 +1,44 @@
+import map from 'lodash/map'
+import styled from 'styled-components/macro'
+
+import { Radio } from 'features/Common'
+import { useCardsStore } from 'features/CardsStore'
+
+const List = styled.ul`
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 20px;
+ margin-bottom: 25px;
+`
+
+const Item = styled.li`
+ width: 50%;
+ height: 50px;
+ display: flex;
+ align-items: center;
+`
+
+export const CardsList = () => {
+ const {
+ cards,
+ defaultCard,
+ onSetDefaultCard,
+ } = useCardsStore()
+
+ return (
+
+ {
+ map(cards, (card) => (
+ -
+ onSetDefaultCard(card.id)}
+ />
+
+ ))
+ }
+
+ )
+}
diff --git a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx
index f759c7ff..f8ff71e9 100644
--- a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx
+++ b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx
@@ -14,7 +14,7 @@ import {
} from '../../styled'
export const ErrorStep = () => {
- const { close } = useBuyMatchPopupStore()
+ const { close, error: paymentError } = useBuyMatchPopupStore()
return (
@@ -27,7 +27,9 @@ export const ErrorStep = () => {
-
+
+ {paymentError || }
+
)
diff --git a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx
index d9c046aa..fb6ec811 100644
--- a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx
+++ b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx
@@ -1,7 +1,8 @@
import styled from 'styled-components/macro'
-import { T9n } from 'features/T9n'
-import { ButtonOutline } from 'features/Common'
+import capitalize from 'lodash/capitalize'
+
+import { useCardsStore } from 'features/CardsStore'
const Wrapper = styled.div`
display: flex;
@@ -17,29 +18,14 @@ const CardInfo = styled.span`
color: rgba(255, 255, 255, 0.7);
`
-const ChangeCardButton = styled(ButtonOutline)`
- border: none;
- padding: 0;
- width: auto;
- height: auto;
-
- padding: 0 10px;
- margin-left: 10px;
- line-height: 20px;
- font-size: 14px;
- color: rgba(255, 255, 255, 0.5);
- cursor: pointer;
+export const SelectedCard = () => {
+ const { defaultCard } = useCardsStore()
- :hover {
- color: rgba(255, 255, 255);
- }
-`
+ if (!defaultCard) return null
-export const SelectedCard = () => (
-
- Mastercard •••• 4432
-
-
-
-
-)
+ return (
+
+ {capitalize(defaultCard?.brand)} •••• {defaultCard?.last4}
+
+ )
+}
diff --git a/src/features/BuyMatchPopup/components/SubscriptionSelectionStep/index.tsx b/src/features/BuyMatchPopup/components/SubscriptionSelectionStep/index.tsx
index de8d09b2..af5a4850 100644
--- a/src/features/BuyMatchPopup/components/SubscriptionSelectionStep/index.tsx
+++ b/src/features/BuyMatchPopup/components/SubscriptionSelectionStep/index.tsx
@@ -1,6 +1,9 @@
+import { useEffect } from 'react'
+
+import isNull from 'lodash/isNull'
+
import { MDASH } from 'config'
-import { T9n } from 'features/T9n'
import {
CloseButton,
Header,
@@ -8,10 +11,12 @@ import {
HeaderTitle,
} from 'features/PopupComponents'
import { Name } from 'features/Name'
+import { useCardsStore } from 'features/CardsStore'
import { useBuyMatchPopupStore } from '../../store'
import { SelectedCard } from '../SelectedCard'
import { Subscriptions } from '../Subscriptions'
+import { Steps } from '../../types'
import {
Wrapper,
Body,
@@ -20,13 +25,24 @@ import {
} from '../../styled'
export const SubscriptionSelectionStep = () => {
+ const {
+ cards,
+ fetchCards,
+ } = useCardsStore()
+
const {
close,
+ goTo,
match,
selectedSubscription,
- subscribeToMatch,
} = useBuyMatchPopupStore()
+ useEffect(() => {
+ if (isNull(cards)) {
+ fetchCards()
+ }
+ }, [cards, fetchCards])
+
if (!match) return null
return (
@@ -48,9 +64,9 @@ export const SubscriptionSelectionStep = () => {
diff --git a/src/features/BuyMatchPopup/components/SubscriptionsList/index.tsx b/src/features/BuyMatchPopup/components/SubscriptionsList/index.tsx
index 1efa60ce..a5510849 100644
--- a/src/features/BuyMatchPopup/components/SubscriptionsList/index.tsx
+++ b/src/features/BuyMatchPopup/components/SubscriptionsList/index.tsx
@@ -1,10 +1,7 @@
import map from 'lodash/map'
import { T9n } from 'features/T9n'
-import type {
- MatchSubscriptions,
- MatchSubscription,
-} from 'features/BuyMatchPopup/types'
+import { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types'
import {
Header,
@@ -17,7 +14,7 @@ import {
type Props = {
onSelect: (subscription: MatchSubscription) => void,
selectedSubscription: MatchSubscription | null,
- subscriptions: MatchSubscriptions,
+ subscriptions: Array,
}
export const SubscriptionsList = ({
@@ -52,7 +49,7 @@ export const SubscriptionsList = ({
)
diff --git a/src/features/BuyMatchPopup/components/SuccessStep/index.tsx b/src/features/BuyMatchPopup/components/SuccessStep/index.tsx
index b7616219..3f9947a8 100644
--- a/src/features/BuyMatchPopup/components/SuccessStep/index.tsx
+++ b/src/features/BuyMatchPopup/components/SuccessStep/index.tsx
@@ -24,7 +24,9 @@ export const SuccessStep = () => {
-
+
+
+