Skip to content

Commit

Permalink
Payments add card update with setup intent (#1584)
Browse files Browse the repository at this point in the history
* adding setup intents

* cards add and setup intent

* styling add card

* recreating coloors

* styling ready

* lint

* Update packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/AddCardModal.tsx

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>

* Update packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/AddCardModal.tsx

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>

* Update packages/files-ui/src/Components/Modules/Settings/SubscriptionTab/AddCardModal.tsx

Co-authored-by: Michael Yankelev <12774278+FSM1@users.noreply.github.com>

* Update packages/files-ui/src/Contexts/BillingContext.tsx

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>

* fixed imports, event types and extra code

* error and loading states

* updated eerror handling system

* handling addition

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>
Co-authored-by: Michael Yankelev <12774278+FSM1@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 4, 2021
1 parent a539565 commit e4591af
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 201 deletions.
2 changes: 2 additions & 0 deletions packages/files-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"@lingui/react": "^3.7.2",
"@material-ui/core": "^4.12.3",
"@sentry/react": "^5.28.0",
"@stripe/react-stripe-js": "^1.4.1",
"@stripe/stripe-js": "^1.18.0",
"@tkey/default": "3.14.2",
"@tkey/security-questions": "3.14.2",
"@tkey/web-storage": "3.14.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Button, Grid, TextInput, Typography, useToasts } from "@chainsafe/common-components"
import { createStyles, makeStyles } from "@chainsafe/common-theme"
import React, { useState, useCallback } from "react"
import React, { FormEvent, useState } from "react"
import { Button, Grid, Typography, useToasts } from "@chainsafe/common-components"
import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme"
import { CSFTheme } from "../../../../Themes/types"
import CustomModal from "../../../Elements/CustomModal"
import CustomButton from "../../../Elements/CustomButton"
import { t, Trans } from "@lingui/macro"
import CardInputs from "../../../Elements/CardInputs"
import { getCardNumberError, getCVCError, getExpiryDateError } from "../../../Elements/CardInputs/utils/validator"
import { useStripe, useElements, CardNumberElement, CardExpiryElement, CardCvcElement } from "@stripe/react-stripe-js"
import { useFilesApi } from "../../../../Contexts/FilesApiContext"
import { useBilling } from "../../../../Contexts/BillingContext"
import clsx from "clsx"

const useStyles = makeStyles(
({ breakpoints, constants, typography, zIndex }: CSFTheme) => {
({ breakpoints, constants, typography, zIndex, palette, animation }: CSFTheme) => {
return createStyles({
root: {
padding: constants.generalUnit * 4,
Expand Down Expand Up @@ -55,71 +56,105 @@ const useStyles = makeStyles(
},
footer: {
marginTop: constants.generalUnit * 4
},
cardNumberInputs: {
marginBottom: constants.generalUnit * 2,
[breakpoints.down("md")]: {
marginTop: constants.generalUnit * 2,
marginBottom: constants.generalUnit * 2
}
},
cardInputs: {
border: `1px solid ${palette.additional["gray"][6]}`,
borderRadius: 2,
padding: constants.generalUnit * 1.5,
transitionDuration: `${animation.transform}ms`,
"&:hover": {
borderColor: palette.primary.border
}
},
cardInputsFocus: {
borderColor: palette.primary.border,
boxShadow: constants.addCard.shadow
},
expiryCvcContainer: {
display: "grid",
gridTemplateColumns: "1fr 1fr",
marginTop: constants.generalUnit,
gridColumnGap: constants.generalUnit,
[breakpoints.down("md")]: {
gridTemplateColumns: "1fr",
gridRowGap: constants.generalUnit * 2
}
},
error: {
marginTop: constants.generalUnit * 2,
color: palette.error.main
}
})
}
)

interface ICreateFolderModalProps {
interface IAddCardModalProps {
isModalOpen: boolean
onClose: () => void
}

const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) => {
const AddCardModal = ({ isModalOpen, onClose }: IAddCardModalProps) => {
const classes = useStyles()
const [cardInputs, setCardInputs] = useState({
cardNumber: "",
cardExpiry: "",
cardCvc: ""
})
const [cardName, setCardName] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const { addCard, getCardTokenFromStripe } = useBilling()
const stripe = useStripe()
const elements = useElements()
const { addToast } = useToasts()
const { filesApiClient } = useFilesApi()
const { refreshDefaultCard } = useBilling()
const [focusElement, setFocusElement] = useState<"number" | "expiry" | "cvc" | undefined>(undefined)
const [cardAddError, setCardAddError] = useState<string | undefined>(undefined)
const theme: CSFTheme = useTheme()

const onCloseModal = useCallback(() => {
setCardInputs({ cardNumber: "", cardExpiry: "", cardCvc: "" })
setCardName("")
setError("")
onClose()
}, [onClose])
const [loadingPaymentMethodAdd, setLoadingPaymentMethodAdd] = useState(false)

const onSubmitCard = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const error = !cardName
? t`Name on card is required`
: getCardNumberError(cardInputs.cardNumber) ||
getExpiryDateError(cardInputs.cardExpiry) ||
getCVCError(cardInputs.cardCvc, cardInputs.cardNumber)
const handleSubmitPaymentMethod = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setCardAddError(undefined)
if (!stripe || !elements) return
try {
const cardNumberElement = elements.getElement(CardNumberElement)
if (!cardNumberElement) return

if (error) {
setError(error)
return
}
setError("")
setLoadingPaymentMethodAdd(true)
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: "card",
card: cardNumberElement
})

setLoading(true)
// get token from stripe
getCardTokenFromStripe(cardInputs).then((resp) => {
// send stripe token to API
addCard(resp.data.id)
.then(() => {
onCloseModal()
addToast({
title: t`Card added successfully`,
type: "success"
})
}).catch((e) => {
setError(t`Something went wrong, please try again`)
console.error(e)
}).finally(() => setLoading(false))
}).catch((err) => {
console.error(err)
setError(t`Card details could not be validated`)
setLoading(false)
})
}, [cardName, cardInputs, getCardTokenFromStripe, addCard, onCloseModal, addToast])
if (error || !paymentMethod) {
console.error(error)
setLoadingPaymentMethodAdd(false)
setCardAddError(t`Card inputs invalid`)
return
}

const setupIntent = await filesApiClient.createSetupIntent()
const setupIntentResult = await stripe.confirmCardSetup(setupIntent.secret, {
payment_method: paymentMethod.id
})

if (setupIntentResult.error || !setupIntentResult.setupIntent) {
console.error(error)
setLoadingPaymentMethodAdd(false)
setCardAddError(t`Failed to add payment method`)
return
}
refreshDefaultCard()
onClose()
setLoadingPaymentMethodAdd(false)
addToast({ title: "Payment method added", type: "success" })
} catch (error) {
console.error(error)
setLoadingPaymentMethodAdd(false)
setCardAddError(t`Failed to add payment method`)
}
}

return (
<CustomModal
Expand All @@ -131,10 +166,10 @@ const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) =>
closePosition="none"
maxWidth="sm"
>
<form onSubmit={onSubmitCard}>
<form onSubmit={handleSubmitPaymentMethod}>
<div
className={classes.root}
data-cy="modal-create-folder"
data-cy="modal-add-card"
>
<Grid
item
Expand All @@ -149,30 +184,60 @@ const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) =>
<Trans>Add a credit card</Trans>
</Typography>
</Grid>
<Grid
item
xs={12}
sm={12}
>
<TextInput
value={cardName}
onChange={(val) =>
setCardName(val?.toString() || "")
<CardNumberElement
className={clsx(
classes.cardInputs,
classes.cardNumberInputs,
focusElement === "number" && classes.cardInputsFocus
)}
options={{ showIcon: true, style: {
base: {
color: theme.constants.addCard.color
}
size="large"
placeholder={t`Name on card`}
label={t`Name on card`}
} }}
onFocus={() => setFocusElement("number")}
onBlur={() => setFocusElement(undefined)}
onChange={() => setCardAddError(undefined)}
/>
<div className={classes.expiryCvcContainer}>
<CardExpiryElement
className={clsx(
classes.cardInputs,
focusElement === "expiry" && classes.cardInputsFocus
)}
onFocus={() => setFocusElement("expiry")}
onBlur={() => setFocusElement(undefined)}
onChange={() => setCardAddError(undefined)}
options={{ style: {
base: {
color: theme.constants.addCard.color
}
} }}
/>
<CardInputs
cardNumber={cardInputs.cardNumber}
cardExpiry={cardInputs.cardExpiry}
cardCvc={cardInputs.cardCvc}
handleChangeCardNumber={(value) => setCardInputs({ ...cardInputs, cardNumber: value })}
handleChangeCardExpiry={(value) => setCardInputs({ ...cardInputs, cardExpiry: value })}
handleChangeCardCvc={(value) => setCardInputs({ ...cardInputs, cardCvc: value })}
error={error}
<CardCvcElement
className={clsx(
classes.cardInputs,
focusElement === "cvc" && classes.cardInputsFocus
)}
onFocus={() => setFocusElement("cvc")}
onBlur={() => setFocusElement(undefined)}
onChange={() => setCardAddError(undefined)}
options={{ style: {
base: {
color: theme.constants.addCard.color
}
} }}
/>
</Grid>
</div>
{cardAddError &&
<Typography
component="p"
variant="body1"
className={classes.error}
>
{cardAddError}
</Typography>
}
<Grid
item
flexDirection="row"
Expand All @@ -181,12 +246,12 @@ const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) =>
>
<CustomButton
data-cy="button-cancel-create-folder"
onClick={onCloseModal}
onClick={onClose}
size="medium"
className={classes.cancelButton}
variant="outline"
type="button"
disabled={loading}
disabled={loadingPaymentMethodAdd}
>
<Trans>Cancel</Trans>
</CustomButton>
Expand All @@ -196,8 +261,8 @@ const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) =>
variant="primary"
type="submit"
className={classes.okButton}
loading={loading}
disabled={loading}
loading={loadingPaymentMethodAdd}
disabled={loadingPaymentMethodAdd}
>
<Trans>Add card</Trans>
</Button>
Expand All @@ -208,4 +273,4 @@ const CreateFolderModal = ({ isModalOpen, onClose }: ICreateFolderModalProps) =>
)
}

export default CreateFolderModal
export default AddCardModal
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import CurrentCard from "./CurrentCard"
import { Divider, Typography } from "@chainsafe/common-components"
import { makeStyles, createStyles, ITheme } from "@chainsafe/common-theme"
import { Trans } from "@lingui/macro"
import { Elements } from "@stripe/react-stripe-js"
import { loadStripe } from "@stripe/stripe-js"

const useStyles = makeStyles(({ constants }: ITheme) =>
createStyles({
Expand All @@ -13,22 +15,26 @@ const useStyles = makeStyles(({ constants }: ITheme) =>
})
)

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PK || "")

const PlanView: React.FC = () => {
const classes = useStyles()

return (
<div>
<Typography
variant="h3"
component="h3"
className={classes.heading}
>
<Trans>Payment and Subscriptions</Trans>
</Typography>
<Divider />
<CurrentProduct />
<CurrentCard />
</div>
<Elements stripe={stripePromise}>
<div>
<Typography
variant="h3"
component="h3"
className={classes.heading}
>
<Trans>Payment and Subscriptions</Trans>
</Typography>
<Divider />
<CurrentProduct />
<CurrentCard />
</div>
</Elements>
)
}

Expand Down
Loading

0 comments on commit e4591af

Please sign in to comment.