diff --git a/client/src/apiCalls.js b/client/src/apiCalls.js index 1c1e830..485c80f 100644 --- a/client/src/apiCalls.js +++ b/client/src/apiCalls.js @@ -63,13 +63,12 @@ export const register = async (username, password, email, onSuccess, onError) => makeRequest("/api/register", "POST", {"Content-Type": "application/json"}, JSON.stringify(data), onSuccess, onError) } -export const createContest = async (title, description, prize_contest, deadline_date, contest_creator, inspirational_images, onSuccess, onError) => { +export const createContest = async (title, description, prize_contest, deadline_date, inspirational_images, onSuccess, onError) => { const data = { 'title': title, 'description': description, 'prize_contest': prize_contest, 'deadline_date': deadline_date, - 'contest_creator': contest_creator, 'inspirational_images': inspirational_images } @@ -89,6 +88,19 @@ export const getStripeID = async (onSuccess, onError) => { return true } +export const checkStripeIDExists = async (onSuccess, onError) => { + get("/api/get_stripe_id", {}, onSuccess, onError) +} + +export const createPayment = async (amount, currency, onSuccess, onError) => { + const data = { + amount: amount, + currency: currency + } + + makeRequest('/api/create_payment', 'POST', {"Content-Type": "application/json"}, JSON.stringify(data), onSuccess, onError) +} + export const getOwnedContests = async (userId, onSuccess, onError) => { get(`/contests/owned/${userId}`, {"Content-Type": "application/json"}, onSuccess, onError) } diff --git a/client/src/components/CheckoutForm.js b/client/src/components/CheckoutForm.js new file mode 100644 index 0000000..8720b8b --- /dev/null +++ b/client/src/components/CheckoutForm.js @@ -0,0 +1,154 @@ +import React, { useState } from 'react' +import { CardNumberElement, CardExpiryElement, CardCvcElement, useElements, useStripe} from '@stripe/react-stripe-js' +import { makeStyles } from "@material-ui/core/styles" +import RequestError, { setContestWinner } from '../apiCalls' + +const CARD_ELEMENT_OPTIONS = { + showIcon: true, + style: { + base: { + color: "#32325d", + fontFamily: '"Helvetica Neue", Helvetica, sans-serif', + fontSmoothing: "antialiased", + fontSize: "16px", + "::placeholder": { + color: "#aab7c4", + }, + }, + invalid: { + color: "#fa755a", + iconColor: "#fa755a", + }, + }, +} + +const useStyles = makeStyles(theme => ({ + root: { + alignItems: "center", + }, + container: { + width:300, + marginTop: theme.spacing(2), + '& .StripeElement': { + minWidth: '400px' + } + }, + centered: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + button: { + marginTop: theme.spacing(5), + padding: '0.5rem' + } +})) + +export default function CheckoutForm(props) { + const [numberError, setNumberError] = useState(null); + const [expiryError, setExpiryError] = useState(null); + const [cvcError, setCVCError] = useState(null); + const stripe = useStripe() + const elements = useElements() + const classes = useStyles() + + const handleSubmit = async (event) => { + event.preventDefault() + + if (!stripe || !elements) { + // Stripe.js has not yet loaded. + // Make sure to disable form submission until Stripe.js has loaded. + return + } + + const result = await stripe.confirmCardPayment(props.clientSecret, { + payment_method: { + card: elements.getElement(CardNumberElement), + billing_details: { + name: 'Jenny Rosen', + }, + } + }) + + if (result.error) { + // Show error to your customer (e.g., insufficient funds) + console.log(result.error.message) + } else { + // The payment has been processed! + if (result.paymentIntent.status === 'succeeded') { + setContestWinner(props.contestId, props.winningSubmission, data => { + console.log('Winner Successfully Declared') + alert('Winner Successfully Declared') + }, error => { + if (error instanceof RequestError && error.status === 400) { + console.log(error.body) + } else { + console.log("unexpected error") + } + }) + } else { + alert('Winner Successfully Declared') + } + } + props.setOpenPaymentDialog(false) + } + + const onChangeNumber = (event) => { + if (event.error) { + setNumberError(event.error.message); + } else { + setNumberError(null); + } + } + + const onChangeExpiry = (event) => { + if (event.error) { + setExpiryError(event.error.message); + } else { + setExpiryError(null); + } + } + + const onChangeCVC = (event) => { + if (event.error) { + setCVCError(event.error.message); + } else { + setCVCError(null); + } + } + + return ( +
+ ) +} \ No newline at end of file diff --git a/client/src/components/CreateContestForm.js b/client/src/components/CreateContestForm.js index 852cf33..485306f 100644 --- a/client/src/components/CreateContestForm.js +++ b/client/src/components/CreateContestForm.js @@ -130,8 +130,7 @@ export default function CreateContestForm() { const minutes = time.substr(3, 5) deadline.setHours(hours) deadline.setMinutes(minutes) - const contestCreator = user.username - createContest(title, description, amount, deadline, contestCreator, checkedInspireationalImages, data => { + createContest(title, description, amount, deadline, checkedInspireationalImages, data => { alert('contest has been successfully created!') history.push(`/contest-details/${data.contest_id}`) }, error => { diff --git a/client/src/pages/ContestDetails.js b/client/src/pages/ContestDetails.js index 3a0fdda..c9a09d0 100644 --- a/client/src/pages/ContestDetails.js +++ b/client/src/pages/ContestDetails.js @@ -3,6 +3,9 @@ import { Link } from 'react-router-dom' import { useHistory } from 'react-router-dom' import { makeStyles } from '@material-ui/core/styles' import TabPanel from '../components/TabPanel' +import CheckoutForm from '../components/CheckoutForm' +import { loadStripe } from '@stripe/stripe-js' +import { Elements } from '@stripe/react-stripe-js' import { Button, Typography, @@ -21,9 +24,10 @@ import { Radio, } from '@material-ui/core' -import RequestError, { getContestDetails, setContestWinner } from '../apiCalls' +import RequestError, { getContestDetails, checkStripeIDExists, createPayment } from '../apiCalls' import { UserContext } from '../App' +const stripePromise = loadStripe("pk_test_51IH8tAGt0nFBLozrVPDB6mwr6yEw9QOAVWhTQK0hErAcLv7F278Dz2f3P637jEaboOp7lJbAVnZNbQTxQUoqD91z00VxrG6s3T") const useStyles = makeStyles((theme) => ({ pageContainer: { height: 'calc(100vh - 100px)', @@ -127,6 +131,17 @@ const useStyles = makeStyles((theme) => ({ dialogImage: { width: '100%', height: '100%' + }, + contestOverText: { + color: 'red', + backgroundColor: '#1f1f1f', + fontSize: '1.5rem', + padding: '1rem' + }, + paymentDialog: { + '& .MuiDialog-paper': { + minWidth: '600px' + } } })) @@ -135,6 +150,7 @@ export default function ContestDetails(props) { const {user, setUser} = useContext(UserContext) const [activeTab, setActiveTab] = useState(0) const [openDesignDialog, setOpenDesignDialog] = useState(false) + const [openPaymentDialog, setOpenPaymentDialog] = useState(false) const [openInspirationalDialog, setOpenInspirationalDialog] = useState(false) const [contest, setContest] = useState(null) const [designOpening, setDesignOpening] = useState({}) @@ -143,6 +159,7 @@ export default function ContestDetails(props) { const [inspirationalGridListItems, setInspirationalGridListItems] = useState(null) const [submitButton, setSubmitButton] = useState(null) const [winningSubmission, setWinningSubmission] = useState(null) + const [clientSecret, setClientSecret] = useState("") const contestId = props.match.params.id const history = useHistory() @@ -155,21 +172,36 @@ export default function ContestDetails(props) { setOpenInspirationalDialog(false); } + const handlePaymentDialogClose = () => { + setOpenPaymentDialog(false) + } + const handleWinnerChange = e => { setWinningSubmission(parseInt(e.target.value)) } - const onWinnerSubmit = e => { - setContestWinner(contestId, winningSubmission, data => { - console.log('Winner Successfully Declared') - }, error => { - if (error instanceof RequestError && error.status === 400) { - console.log(error.body) - } else { - console.log("unexpected error") + const onWinnerSubmit = async(e) => { + checkStripeIDExists( // check if payment is set up before allow user submit a winner + (data) => { + let intentSecret = data["intent_id"] + if (intentSecret != null){ + const amount = contest.prize_contest + const currency = 'usd' + + createPayment(amount, currency, data => { + setClientSecret(data.client_secret) + setOpenPaymentDialog(true) + }, error => { + console.log("unexpected error") + }) + } else { + alert("Please set up your payment before selecting a winner.") + } + }, + (error) => { + alert("Please set up your payment before selecting a winner.") } - }) - getContestInfo(contestId) + ) } const onBackButtonClick = e => { @@ -198,7 +230,6 @@ export default function ContestDetails(props) { const getContestInfo = contestId => { getContestDetails(contestId, (data) => { - console.log(data) data.title ? setContest(data) : setContest(null) }, (error) => { if (error instanceof RequestError && error.status === 400) { @@ -214,8 +245,17 @@ export default function ContestDetails(props) { // eslint-disable-next-line }, []) + useEffect(() => { + //when payment dialog close, should fetch new data + getContestInfo(contestId) + }, [openPaymentDialog, contestId]) + useEffect(() => { if (contest) { + // Hide submit button when contest is over + if (contest.winner !== null) { + setSubmitButton(null) + } var newGridListItems = null const newInspirationalGridListItems = contest.attached_inspirational_images.map((design, index) => (