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 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 { |
||||
Form, |
||||
Column, |
||||
ButtonsBlock, |
||||
Input, |
||||
} from './styled' |
||||
import { useToggle } from 'hooks' |
||||
|
||||
export const AddCardForm = () => ( |
||||
<Form> |
||||
<Column> |
||||
<Input |
||||
labelWidth={70} |
||||
wrapperWidth={560} |
||||
label='Номер' |
||||
labelLexic='form_card_number' |
||||
/> |
||||
<Input |
||||
labelWidth={70} |
||||
wrapperWidth={560} |
||||
label='Имя' |
||||
/> |
||||
<Input |
||||
maxLength={5} |
||||
labelWidth={120} |
||||
wrapperWidth={275} |
||||
labelLexic='form_card_expiration' |
||||
/> |
||||
<Input |
||||
maxLength={3} |
||||
labelWidth={140} |
||||
wrapperWidth={275} |
||||
label='CVC / CVV' |
||||
labelLexic='form_card_code' |
||||
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 |
||||
? ( |
||||
<AddCardFormInner |
||||
inputsBackground={inputsBackground} |
||||
onAddSuccess={toggle} |
||||
submitButton={submitButton} |
||||
/> |
||||
</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 isNumber from 'lodash/isNumber' |
||||
import reduce from 'lodash/reduce' |
||||
|
||||
import { currencySymbols } from 'config' |
||||
|
||||
import type { MatchSubscriptionsResponse } from 'requests' |
||||
|
||||
import { SubscriptionType } from '../types' |
||||
import { SubscriptionType, MatchSubscriptions } from '../types' |
||||
|
||||
export const transformSubsciptions = (subscriptions: MatchSubscriptionsResponse) => ( |
||||
map(subscriptions, (subscription) => ({ |
||||
export const transformSubsciptions = ( |
||||
subscriptions: MatchSubscriptionsResponse, |
||||
) => ( |
||||
reduce( |
||||
subscriptions, |
||||
( |
||||
acc, |
||||
periodSubscriptions, |
||||
type, |
||||
) => { |
||||
const period = type as SubscriptionType |
||||
acc[period] = map(periodSubscriptions, (subscription) => ({ |
||||
currency: currencySymbols[subscription.currency_iso || 'RUB'], |
||||
id: subscription.id, |
||||
lexic: subscription.lexic, |
||||
price: subscription.price_month || subscription.price_year || 0, |
||||
type: isNumber(subscription.price_month) ? SubscriptionType.Month : SubscriptionType.Year, |
||||
type: period, |
||||
...subscription, |
||||
})) |
||||
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