Ott 1049 stripe payments (#390)
* Ott 1028 get token (#353) * feat(1028): get card token * fix(1028): fixed lang type * Ott 1028 card crud (#354) * feat(1028): added card requests * feat(1028): crud on card * feat(1028): buy subs. (#355) * feat(1072): production configuration (#356) * feat(1051): display payment and card link errors (#358) * Ott 1073 one time subs (#359) * refactor(1073): migrate to new proc response type * feat(1073): one time payments * Ott 1066 display default card (#360) * refactor(1066): display default card in subs list popup * fix(1066): test fix * feat(1080): 3ds (#361) * feat(1080): wip, 3ds * fix(1080): pr comments * refactor: url change (#382)keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
parent
93b791612a
commit
a0219f8212
@ -1 +1,3 @@ |
|||||||
export const isProduction = process.env.REACT_APP_PRODUCTION === 'true' |
export const isProduction = process.env.REACT_APP_PRODUCTION === 'true' |
||||||
|
|
||||||
|
export const STRIPE_PUBLIC_KEY = process.env.REACT_APP_STRIPE_PK || 'pk_test_fkEjSoWfJXuCwMgwHRpbOGPt' |
||||||
|
|||||||
@ -0,0 +1,6 @@ |
|||||||
|
export const paymentLexics = { |
||||||
|
add_card: 8313, |
||||||
|
card_holder_name: 2021, |
||||||
|
error_can_not_add_card: 14447, |
||||||
|
error_payment_unsuccessful: 14446, |
||||||
|
} |
||||||
@ -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<LabelProps>` |
||||||
|
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) => ( |
||||||
|
<Label width={width} backgroundColor={backgroundColor}> |
||||||
|
{label && <Text t={label} />} |
||||||
|
<ElementWrapper> |
||||||
|
{children} |
||||||
|
</ElementWrapper> |
||||||
|
</Label> |
||||||
|
) |
||||||
@ -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<HTMLFormElement>) => { |
||||||
|
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 } |
||||||
|
} |
||||||
@ -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 ( |
||||||
|
<Form onSubmit={handleSubmit}> |
||||||
|
<Column> |
||||||
|
<ElementContainer |
||||||
|
label='form_card_number' |
||||||
|
backgroundColor={inputsBackground} |
||||||
|
> |
||||||
|
<CardNumberElement options={options} /> |
||||||
|
</ElementContainer> |
||||||
|
|
||||||
|
<ElementContainer |
||||||
|
label='card_holder_name' |
||||||
|
backgroundColor={inputsBackground} |
||||||
|
> |
||||||
|
<Input name='cardHolderName' /> |
||||||
|
</ElementContainer> |
||||||
|
|
||||||
|
<ElementContainer |
||||||
|
label='form_card_expiration' |
||||||
|
width='275px' |
||||||
|
backgroundColor={inputsBackground} |
||||||
|
> |
||||||
|
<CardExpiryElement options={options} /> |
||||||
|
</ElementContainer> |
||||||
|
|
||||||
|
<ElementContainer |
||||||
|
label='form_card_code' |
||||||
|
width='275px' |
||||||
|
backgroundColor={inputsBackground} |
||||||
|
> |
||||||
|
<CardCvcElement options={options} /> |
||||||
|
</ElementContainer> |
||||||
|
</Column> |
||||||
|
<ButtonsBlock> |
||||||
|
{(formError || cardError) && ( |
||||||
|
<Errors> |
||||||
|
{formError} |
||||||
|
<T9n t={cardError} /> |
||||||
|
</Errors> |
||||||
|
) } |
||||||
|
|
||||||
|
<SubmitButton type='submit'> |
||||||
|
Сохранить |
||||||
|
</SubmitButton> |
||||||
|
</ButtonsBlock> |
||||||
|
</Form> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,44 +1,42 @@ |
|||||||
import { SolidButton } from 'features/UserAccount/styled' |
import type { MouseEvent } from 'react' |
||||||
|
|
||||||
import { |
import { useToggle } from 'hooks' |
||||||
Form, |
|
||||||
Column, |
|
||||||
ButtonsBlock, |
|
||||||
Input, |
|
||||||
} from './styled' |
|
||||||
|
|
||||||
export const AddCardForm = () => ( |
import { T9n } from 'features/T9n' |
||||||
<Form> |
import { OutlineButton, Icon } from 'features/UserAccount/styled' |
||||||
<Column> |
|
||||||
<Input |
import type { Props } from './components/Form/hooks' |
||||||
labelWidth={70} |
import { AddCardFormInner } from './components/Form' |
||||||
wrapperWidth={560} |
|
||||||
label='Номер' |
export const AddCardForm = ({ |
||||||
labelLexic='form_card_number' |
initialformOpen, |
||||||
/> |
inputsBackground, |
||||||
<Input |
submitButton, |
||||||
labelWidth={70} |
}: Props) => { |
||||||
wrapperWidth={560} |
const { isOpen, toggle } = useToggle(initialformOpen) |
||||||
label='Имя' |
|
||||||
/> |
const onAddClick = (e: MouseEvent) => { |
||||||
<Input |
e.stopPropagation() |
||||||
maxLength={5} |
toggle() |
||||||
labelWidth={120} |
} |
||||||
wrapperWidth={275} |
|
||||||
labelLexic='form_card_expiration' |
return ( |
||||||
/> |
isOpen |
||||||
<Input |
? ( |
||||||
maxLength={3} |
<AddCardFormInner |
||||||
labelWidth={140} |
inputsBackground={inputsBackground} |
||||||
wrapperWidth={275} |
onAddSuccess={toggle} |
||||||
label='CVC / CVV' |
submitButton={submitButton} |
||||||
labelLexic='form_card_code' |
|
||||||
/> |
/> |
||||||
</Column> |
|
||||||
<ButtonsBlock> |
|
||||||
<SolidButton type='submit'> |
|
||||||
Сохранить |
|
||||||
</SolidButton> |
|
||||||
</ButtonsBlock> |
|
||||||
</Form> |
|
||||||
) |
) |
||||||
|
: ( |
||||||
|
<OutlineButton |
||||||
|
type='button' |
||||||
|
onClick={onAddClick} |
||||||
|
> |
||||||
|
<Icon src='plusIcon' /> |
||||||
|
<T9n t='add_card' /> |
||||||
|
</OutlineButton> |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
|||||||
@ -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 ( |
||||||
|
<Wrapper width={630}> |
||||||
|
<Header height={50}> |
||||||
|
<HeaderTitle> |
||||||
|
{emptyCards ? 'Добавление карты' : 'Выберите карту для оплаты'} |
||||||
|
</HeaderTitle> |
||||||
|
<HeaderActions position='right'> |
||||||
|
<CloseButton onClick={close} /> |
||||||
|
</HeaderActions> |
||||||
|
</Header> |
||||||
|
<Body padding='0 35px' marginBottom={40}> |
||||||
|
<CardsList /> |
||||||
|
<AddCardForm |
||||||
|
initialformOpen={emptyCards} |
||||||
|
submitButton='outline' |
||||||
|
inputsBackground='rgba(255, 255, 255, 0.1)' |
||||||
|
/> |
||||||
|
</Body> |
||||||
|
<Footer> |
||||||
|
{!emptyCards && ( |
||||||
|
<Button onClick={subscribeToMatch}> |
||||||
|
<T9n t='buy_subscription' /> |
||||||
|
</Button> |
||||||
|
)} |
||||||
|
</Footer> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -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 ( |
||||||
|
<List> |
||||||
|
{ |
||||||
|
map(cards, (card) => ( |
||||||
|
<Item key={card.id}> |
||||||
|
<Radio |
||||||
|
label={`${card.brand} •••• ${card.last4}`} |
||||||
|
checked={card.id === defaultCard?.id} |
||||||
|
onChange={() => onSetDefaultCard(card.id)} |
||||||
|
/> |
||||||
|
</Item> |
||||||
|
)) |
||||||
|
} |
||||||
|
</List> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,18 +1,30 @@ |
|||||||
import map from 'lodash/map' |
import map from 'lodash/map' |
||||||
import isNumber from 'lodash/isNumber' |
import reduce from 'lodash/reduce' |
||||||
|
|
||||||
import { currencySymbols } from 'config' |
import { currencySymbols } from 'config' |
||||||
|
|
||||||
import type { MatchSubscriptionsResponse } from 'requests' |
import type { MatchSubscriptionsResponse } from 'requests' |
||||||
|
|
||||||
import { SubscriptionType } from '../types' |
import { SubscriptionType, MatchSubscriptions } from '../types' |
||||||
|
|
||||||
export const transformSubsciptions = (subscriptions: MatchSubscriptionsResponse) => ( |
export const transformSubsciptions = ( |
||||||
map(subscriptions, (subscription) => ({ |
subscriptions: MatchSubscriptionsResponse, |
||||||
|
) => ( |
||||||
|
reduce( |
||||||
|
subscriptions, |
||||||
|
( |
||||||
|
acc, |
||||||
|
periodSubscriptions, |
||||||
|
type, |
||||||
|
) => { |
||||||
|
const period = type as SubscriptionType |
||||||
|
acc[period] = map(periodSubscriptions, (subscription) => ({ |
||||||
currency: currencySymbols[subscription.currency_iso || 'RUB'], |
currency: currencySymbols[subscription.currency_iso || 'RUB'], |
||||||
id: subscription.id, |
type: period, |
||||||
lexic: subscription.lexic, |
...subscription, |
||||||
price: subscription.price_month || subscription.price_year || 0, |
|
||||||
type: isNumber(subscription.price_month) ? SubscriptionType.Month : SubscriptionType.Year, |
|
||||||
})) |
})) |
||||||
|
return acc |
||||||
|
}, |
||||||
|
{} as MatchSubscriptions, |
||||||
|
) |
||||||
) |
) |
||||||
|
|||||||
@ -0,0 +1,20 @@ |
|||||||
|
import type { PaymentIntentResult } from '@stripe/stripe-js' |
||||||
|
import { useStripe } from '@stripe/react-stripe-js' |
||||||
|
|
||||||
|
export const useStripe3DSecure = () => { |
||||||
|
const stripe = useStripe() |
||||||
|
|
||||||
|
const handleConfirmationResult = (result: PaymentIntentResult) => { |
||||||
|
if (result.error) { |
||||||
|
return Promise.reject(result.error) |
||||||
|
} |
||||||
|
|
||||||
|
return Promise.resolve(result.paymentIntent) |
||||||
|
} |
||||||
|
|
||||||
|
const handle3DSecure = (clientSecret: string) => ( |
||||||
|
stripe?.confirmCardPayment(clientSecret).then(handleConfirmationResult) |
||||||
|
) |
||||||
|
|
||||||
|
return { handle3DSecure } |
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useState, |
||||||
|
useMemo, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import find from 'lodash/find' |
||||||
|
|
||||||
|
import type { Cards } from 'requests/getCardsList' |
||||||
|
import { getCardsList } from 'requests/getCardsList' |
||||||
|
import { addCard } from 'requests/addCard' |
||||||
|
import { deleteCard } from 'requests/deleteCard' |
||||||
|
import { setDefaultCard as setDefaultCardRequest } from 'requests/setDefaultCard' |
||||||
|
|
||||||
|
export const useBankCards = () => { |
||||||
|
const [error, setError] = useState('') |
||||||
|
const [cards, setCards] = useState<Cards | null>(null) |
||||||
|
const defaultCard = useMemo( |
||||||
|
() => find(cards, { default: true }), |
||||||
|
[cards], |
||||||
|
) |
||||||
|
|
||||||
|
const fetchCards = useCallback(() => getCardsList().then(setCards), []) |
||||||
|
|
||||||
|
const onAddCard = async (token: string) => ( |
||||||
|
addCard(token) |
||||||
|
.catch(() => { |
||||||
|
setError('error_can_not_add_card') |
||||||
|
return Promise.reject() |
||||||
|
}) |
||||||
|
.then(fetchCards) |
||||||
|
) |
||||||
|
|
||||||
|
const onDeleteCard = (cardId: string) => { |
||||||
|
deleteCard(cardId).then(fetchCards) |
||||||
|
} |
||||||
|
|
||||||
|
const onSetDefaultCard = (cardId: string) => { |
||||||
|
setDefaultCardRequest(cardId).then(fetchCards) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
cards, |
||||||
|
defaultCard, |
||||||
|
error, |
||||||
|
fetchCards, |
||||||
|
onAddCard, |
||||||
|
onDeleteCard, |
||||||
|
onSetDefaultCard, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
import type { ReactNode } from 'react' |
||||||
|
import { createContext, useContext } from 'react' |
||||||
|
|
||||||
|
import { useBankCards } from './hooks' |
||||||
|
|
||||||
|
type Context = ReturnType<typeof useBankCards> |
||||||
|
type Props = { children: ReactNode } |
||||||
|
|
||||||
|
const CardsContext = createContext({} as Context) |
||||||
|
|
||||||
|
export const CardsStore = ({ children }: Props) => { |
||||||
|
const value = useBankCards() |
||||||
|
return ( |
||||||
|
<CardsContext.Provider value={value}> |
||||||
|
{children} |
||||||
|
</CardsContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const useCardsStore = () => useContext(CardsContext) |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
import type { ReactNode } from 'react' |
||||||
|
|
||||||
|
import { Elements } from '@stripe/react-stripe-js' |
||||||
|
import { loadStripe } from '@stripe/stripe-js' |
||||||
|
|
||||||
|
import { STRIPE_PUBLIC_KEY } from 'config/env' |
||||||
|
|
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
const stripe = loadStripe(STRIPE_PUBLIC_KEY) |
||||||
|
|
||||||
|
type Props = { |
||||||
|
children: ReactNode, |
||||||
|
} |
||||||
|
|
||||||
|
export const StripeElements = ({ children }: Props) => { |
||||||
|
const { lang } = useLexicsStore() |
||||||
|
|
||||||
|
return ( |
||||||
|
<Elements stripe={stripe} options={{ locale: lang }}> |
||||||
|
{children} |
||||||
|
</Elements> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
export const addCard = async (token: string) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
token, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/attach-card`, |
||||||
|
}) |
||||||
|
} |
||||||
@ -1,24 +0,0 @@ |
|||||||
import { API_ROOT } from 'config' |
|
||||||
import { callApi } from 'helpers' |
|
||||||
|
|
||||||
import type { SubscriptionType } from 'features/BuyMatchPopup/types' |
|
||||||
|
|
||||||
type Subscription = { |
|
||||||
id: number, |
|
||||||
type: SubscriptionType, |
|
||||||
} |
|
||||||
|
|
||||||
export const buyMatchSubscription = ({ id, type }: Subscription) => { |
|
||||||
const config = { |
|
||||||
body: { |
|
||||||
interval: type, |
|
||||||
is_scheduled: 0, |
|
||||||
subscription_plan: id, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
return callApi({ |
|
||||||
config, |
|
||||||
url: `${API_ROOT}/account/purchase`, |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -0,0 +1,24 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import type { SubscriptionResponse } from '../getSubscriptions' |
||||||
|
import { handleUnsuccessfulSubscription } from './handleUnsuccessfulSubscription' |
||||||
|
|
||||||
|
type Args = { |
||||||
|
cardId: string, |
||||||
|
item: SubscriptionResponse, |
||||||
|
} |
||||||
|
|
||||||
|
export const buyMatchPayOnceSubscription = ({ cardId, item }: Args) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
card_id: cardId, |
||||||
|
item, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/pay`, |
||||||
|
}).catch(handleUnsuccessfulSubscription) |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import type { SubscriptionResponse } from 'requests/getSubscriptions' |
||||||
|
|
||||||
|
import { handleUnsuccessfulSubscription } from './handleUnsuccessfulSubscription' |
||||||
|
|
||||||
|
type Subscription = { |
||||||
|
cardId: string, |
||||||
|
item: SubscriptionResponse, |
||||||
|
} |
||||||
|
|
||||||
|
export const buyMatchSubscription = ({ cardId, item }: Subscription) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
card_id: cardId, |
||||||
|
item, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/subscription`, |
||||||
|
}).catch(handleUnsuccessfulSubscription) |
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
type UnsuccessfulResponse = { |
||||||
|
data?: { |
||||||
|
client_secret?: string, |
||||||
|
redirect_to_url?: string, |
||||||
|
use_stripe_sdk?: string, |
||||||
|
}, |
||||||
|
reason: string, |
||||||
|
} |
||||||
|
|
||||||
|
export enum PaymentActions { |
||||||
|
ThreeDSecure, |
||||||
|
Redirect, |
||||||
|
} |
||||||
|
|
||||||
|
export type OnFailedPaymentActionData = { |
||||||
|
action: PaymentActions, |
||||||
|
value: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const handleUnsuccessfulSubscription = ({ data, reason }: UnsuccessfulResponse) => { |
||||||
|
const clientSecret = data?.use_stripe_sdk || data?.client_secret |
||||||
|
if (reason === 'requires_action' && clientSecret) { |
||||||
|
const actionData: OnFailedPaymentActionData = { |
||||||
|
action: PaymentActions.ThreeDSecure, |
||||||
|
value: clientSecret, |
||||||
|
} |
||||||
|
return Promise.reject(actionData) |
||||||
|
} |
||||||
|
|
||||||
|
if (reason === 'requires_action' && data?.redirect_to_url) { |
||||||
|
const actionData: OnFailedPaymentActionData = { |
||||||
|
action: PaymentActions.Redirect, |
||||||
|
value: data.redirect_to_url, |
||||||
|
} |
||||||
|
return Promise.reject(actionData) |
||||||
|
} |
||||||
|
|
||||||
|
return Promise.reject() |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export * from './buyMatchPayOnceSubscription' |
||||||
|
export * from './buyMatchSubscriptions' |
||||||
|
export * from './handleUnsuccessfulSubscription' |
||||||
|
export * from './notifySuccessfulSubscription' |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import type { SubscriptionResponse } from '../getSubscriptions' |
||||||
|
|
||||||
|
type Args = { |
||||||
|
item: SubscriptionResponse, |
||||||
|
paymentIntentId: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const notifySuccessfulSubscription = ({ item, paymentIntentId }: Args) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
item, |
||||||
|
pi_id: paymentIntentId, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/pay-finish`, |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
export const deleteCard = async (cardId: string) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
card_id: cardId, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/delete-card`, |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
export type Card = { |
||||||
|
brand: string, |
||||||
|
default: boolean, |
||||||
|
id: string, |
||||||
|
last4: string, |
||||||
|
} |
||||||
|
|
||||||
|
export type Cards = Array<Card> |
||||||
|
|
||||||
|
export const getCardsList = async (): Promise<Cards> => { |
||||||
|
const config = { |
||||||
|
method: 'GET', |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/get-cards`, |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
import { API_ROOT } from 'config' |
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
export const setDefaultCard = async (cardId: string) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
card_id: cardId, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ |
||||||
|
config, |
||||||
|
url: `${API_ROOT}/account/set-default-card`, |
||||||
|
}) |
||||||
|
} |
||||||
Loading…
Reference in new issue