diff --git a/.gitignore b/.gitignore index 5965314..bf30b61 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Local Netlify folder +.netlify diff --git a/netlify/functions/create-payment-intent.js b/netlify/functions/create-payment-intent.js new file mode 100644 index 0000000..32c5f63 --- /dev/null +++ b/netlify/functions/create-payment-intent.js @@ -0,0 +1,26 @@ +require("dotenv").config(); +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); + +exports.handler = async (event) => { + try { + const { amount } = JSON.parse(event.body); + + const paymentIntent = await stripe.paymentIntents.create({ + amount, + currency: "usd", + payment_method_types: ["card"], + }); + + return { + statusCode: 200, + body: JSON.stringify({ paymentIntent }), + }; + } catch (error) { + console.log({ error }); + + return { + statusCode: 400, + body: JSON.stringify({ error }), + }; + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1f8444a..174c0ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "components": "^0.1.0", + "dotenv": "^16.0.3", "firebase": "^9.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -30,6 +31,7 @@ "redux-thunk": "^2.4.2", "reselect": "^4.1.8", "sass": "^1.62.0", + "stripe": "^12.7.0", "styled-components": "^5.3.11", "web-vitals": "^2.1.4" } @@ -7575,6 +7577,14 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", @@ -16628,6 +16638,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.7.0.tgz", + "integrity": "sha512-4yY9gNFznBI6fZtR0B4y2a+o2EJe+s7p4DiHECKphwa36zYIczpWzO1g4dF0PF3lhTRw5FWTaHGBjwaD5FEiDQ==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/style-loader": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", diff --git a/package.json b/package.json index e29cc3e..01786d5 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "components": "^0.1.0", + "dotenv": "^16.0.3", "firebase": "^9.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -25,6 +26,7 @@ "redux-thunk": "^2.4.2", "reselect": "^4.1.8", "sass": "^1.62.0", + "stripe": "^12.7.0", "styled-components": "^5.3.11", "web-vitals": "^2.1.4" }, diff --git a/src/components/button/button.component.jsx b/src/components/button/button.component.jsx index f20005a..43f260f 100644 --- a/src/components/button/button.component.jsx +++ b/src/components/button/button.component.jsx @@ -1,6 +1,6 @@ import React from 'react' import './button.styles.jsx' -import { BaseButton, GoogleSignInButton, InvertedButton } from './button.styles.jsx' +import { BaseButton, GoogleSignInButton, InvertedButton, LoadingSpinner } from './button.styles.jsx' export const BUTTON_TYPE_CLASS = { base: 'base', google: 'google-sign-in', @@ -16,12 +16,12 @@ const getButton = (buttonType = BUTTON_TYPE_CLASS.base) => ({ ) -export const Button = ({ children, buttonType, ...otherProps }) => { +export const Button = ({ children, buttonType, isLoading, ...otherProps }) => { const CustomButton = getButton(buttonType) return ( - - {children} + + {isLoading ? : children} ) diff --git a/src/components/button/button.styles.jsx b/src/components/button/button.styles.jsx index f4ae485..3f8fdcf 100644 --- a/src/components/button/button.styles.jsx +++ b/src/components/button/button.styles.jsx @@ -17,6 +17,7 @@ export const BaseButton = styled.button` cursor: pointer; display: flex; justify-content: center; + align-items: center; &:hover { background-color: white; @@ -45,4 +46,24 @@ export const InvertedButton = styled(BaseButton)` color: white; border: none; } +`; +export const LoadingSpinner = styled.div` + display: inline-block; + width: 30px; + height: 30px; + border: 3px solid rgba(195, 195, 195, 0.6); + border-radius: 50%; + border-top-color: #636767; + animation: spin 1s ease-in-out infinite; + -webkit-animation: spin 1s ease-in-out infinite; + @keyframes spin { + to { + -webkit-transform: rotate(360deg); + } + } + @-webkit-keyframes spin { + to { + -webkit-transform: rotate(360deg); + } + } `; \ No newline at end of file diff --git a/src/components/payment-form/payment.form.jsx b/src/components/payment-form/payment.form.jsx index 5c9df7d..396e3c0 100644 --- a/src/components/payment-form/payment.form.jsx +++ b/src/components/payment-form/payment.form.jsx @@ -1,28 +1,58 @@ import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js"; import { BUTTON_TYPE_CLASS, Button } from "../button/button.component"; -import { FormContainer, PaymentFormContainer } from "./payment.style"; +import { FormContainer, PaymentButton, PaymentFormContainer } from "./payment.style"; +import { useSelector } from "react-redux"; +import { selectCartTotal } from "../../store/cart/cart.selector"; +import { selectCurrentUser } from "../../store/user/user.selector"; +import { useState } from "react"; export const PaymentForm = () => { const stripe = useStripe() const elements = useElements() + const amount = useSelector(selectCartTotal) + const currentUser = useSelector(selectCurrentUser) + const [isProcessingPayment, setIsProcessingPayment] = useState(false) - - - const paymentHandler = (e) => { + const paymentHandler = async (e) => { e.preventDefault() if (!stripe || !elements) { return } + setIsProcessingPayment(true) + const response = await fetch("/.netlify/functions/create-payment-intent", { + method: "post", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ amount: amount * 100 }) + }).then(res => res.json()) + const clientSecret = response.paymentIntent.client_secret + const paymentResult = await stripe.confirmCardPayment(clientSecret, { + payment_method: { + card: elements.getElement(CardElement), + billing_details: { + name: currentUser ? currentUser.displayName : 'Guest' + } + } + }) + setIsProcessingPayment(false) + if (paymentResult.error) { + alert(paymentResult.error) + } else { + if (paymentResult.paymentIntent.status === 'succeeded') { + alert("Payment succeeded") + } + } } return ( - +

Credit Card Payment :

- + Pay Now
) diff --git a/src/components/payment-form/payment.style.jsx b/src/components/payment-form/payment.style.jsx index a12e229..58a8d0a 100644 --- a/src/components/payment-form/payment.style.jsx +++ b/src/components/payment-form/payment.style.jsx @@ -1,4 +1,5 @@ import styled from 'styled-components' +import { Button } from '../button/button.component' export const PaymentFormContainer = styled.div` height: 300px; @@ -7,7 +8,11 @@ export const PaymentFormContainer = styled.div` align-items : center; justify-content: center; ` -export const FormContainer = styled.div` +export const FormContainer = styled.form` height: 100px; min-width:500px; +` +export const PaymentButton = styled(Button)` + margin-left: auto; + margin-top: 30px; ` \ No newline at end of file