diff --git a/.gitignore b/.gitignore index 99048f8..ec8dc6f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ build/ dist/ *.pyc *.pyo -**/env/ +.env/ env/ .env mCovo.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/ax3Client.iml b/.idea/ax3Client.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/ax3Client.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..be27107 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c8c7cbc..2ff90c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "client", + "name": "novodraft-client", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "client", + "name": "novodraft-client", "version": "0.1.0", "dependencies": { "@testing-library/jest-dom": "^5.17.0", @@ -14,6 +14,7 @@ "bootstrap": "^5.3.2", "classnames": "^2.3.2", "docx": "^8.2.4", + "dotenv": "^16.3.2", "firebase": "^10.3.1", "react": "^18.2.0", "react-bootstrap": "^2.8.0", @@ -24,6 +25,7 @@ "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", "react-spinners": "^0.13.8", + "stripe": "^14.13.0", "uuid": "^9.0.1", "web-vitals": "^2.1.4" }, @@ -7849,6 +7851,17 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", @@ -17275,6 +17288,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "14.13.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.13.0.tgz", + "integrity": "sha512-uLOfWtBUL1amJCTpKCXWrHntFOSaO2PWb/2hsxV/OlXLr0bz5MyU8IW1pFlmZqpw6hBqAW5Fad7Ty7xRxDYrzA==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", diff --git a/package.json b/package.json index 55f1a87..fcc1946 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bootstrap": "^5.3.2", "classnames": "^2.3.2", "docx": "^8.2.4", + "dotenv": "^16.3.2", "firebase": "^10.3.1", "react": "^18.2.0", "react-bootstrap": "^2.8.0", @@ -20,6 +21,7 @@ "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", "react-spinners": "^0.13.8", + "stripe": "^14.13.0", "uuid": "^9.0.1", "web-vitals": "^2.1.4" }, diff --git a/src/Components/SignupPage/SignupPage.js b/src/Components/SignupPage/SignupPage.js index 900991c..7834952 100644 --- a/src/Components/SignupPage/SignupPage.js +++ b/src/Components/SignupPage/SignupPage.js @@ -17,25 +17,34 @@ import { isFormDataHasErrors, } from "../../Utils/Form"; import { objectMap } from "../../Utils/Object"; -import { signupfields } from "../../Constants/Fields/SignupFields"; +import { signupFields } from "../../Constants/Fields/SignupFields"; import { paymentfields } from "../../Constants/Fields/PaymentFields"; import PaymentModal from "../Modals/PaymentModal"; import "../../styles/signup.scss"; +import Stripe from 'stripe'; const SignupPage = () => { const navigate = useNavigate(); const [notice, setNotice] = useState(""); const fieldsChunkSize = 2; const [isBusy, setIsBusy] = useState(false); - const [data, setData] = useState(getFormDataDefaults(signupfields)); + const [data, setData] = useState(getFormDataDefaults(signupFields)); const [paymentData, setPaymentData] = useState( getFormDataDefaults(paymentfields) ); const [showPaymentModal, setShowPaymentModal] = useState(false); const modalText = "Description of monthly and annual subscriptions"; + // this could also go in App.js if needed in the future + const stripe = new Stripe('pk_test_51NNoasBi8p7FeGFrI5SfpM6HuNMoxwImx6NRKyKbgbt6OPxMxQDiZ9I1GqvDa9qUwB3D3jlJOng6MyjPppWofxzP00Exvr8dBy') + + const apiUrl = + process.env.NODE_ENV === "development" + ? process.env.REACT_APP_API_DEV + : process.env.REACT_APP_API_PROD; + const handleChangeInput = (e, name) => { - const newData = handleFormDataChange(e, name, data, signupfields); + const newData = handleFormDataChange(e, name, data, signupFields); if (newData !== null) { setData(newData); } @@ -54,7 +63,7 @@ const SignupPage = () => { }; const validateData = () => { - const newData = getValidatedFormData(data, signupfields); + const newData = getValidatedFormData(data, signupFields); const hasErrors = isFormDataHasErrors(newData); setData(newData); return hasErrors ? null : objectMap(({ value }) => value, newData); @@ -125,10 +134,45 @@ const SignupPage = () => { setShowPaymentModal(true); }; - async function handleStripeAuthorization(paymentDataValues) { + async function handleStripeAuthorization(paymentDataValues, customerDataValues) { // Do: calls to Stripe API // if success, return some truthy value or // handle failure in handleSignup + + let error = false; + + const token = await stripe.tokens.create({ + card: { + number: paymentDataValues.cardNumber, + exp_month: paymentDataValues.cardExpirationMonth, + exp_year: paymentDataValues.cardExpirationYear, + cvc: paymentDataValues.cardCvvCode, + name: paymentDataValues.cardFirstName + ' ' + paymentDataValues.cardLastName, + } + }) + + try { + let response = await fetch(`${apiUrl}/create-subscription`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'monthly', // TODO: replace this with the user-chosen value later when we have the UI for it + customerData: { + email: customerDataValues.email, + name: customerDataValues.firstName + ' ' + customerDataValues.lastName, + }, + token, + }), + }) + } + catch (error) { + console.log(error) + error = true; + } + + return !error; } const handleSignup = async (e) => { @@ -144,25 +188,29 @@ const SignupPage = () => { } setIsBusy(true); - const paymentSuccess = handleStripeAuthorization(paymentDataValues); - if (paymentSuccess) { - try { - const userCredential = await createUserWithEmailAndPassword( - auth, - dataValues.email, - dataValues.password - ); - const user = userCredential.user; + + try { + const userCredential = await createUserWithEmailAndPassword( + auth, + dataValues.email, + dataValues.password + ); + const user = userCredential.user; + + const paymentSuccess = await handleStripeAuthorization(paymentDataValues, dataValues); + + if(paymentSuccess) { await saveUserData(user.uid, dataValues); navigate("/"); - } catch (error) { - setIsBusy(false); - setNotice( - error.message || "Sorry, something went wrong. Please try again." - ); } - } else { - //handle payment failure + else { + // handle payment failure + } + } catch (error) { + setIsBusy(false); + setNotice( + error.message || "Sorry, something went wrong. Please try again." + ); } }; @@ -174,7 +222,7 @@ const SignupPage = () => {
{splitEvery( - Object.keys(signupfields).slice(0, -2), + Object.keys(signupFields).slice(0, -2), fieldsChunkSize ).map((names, j) => ( @@ -189,11 +237,11 @@ const SignupPage = () => { message={data[name].message} label={ data[name].value.length === 0 - ? signupfields[name].label + ? signupFields[name].label : "" } - type={signupfields[name].type} - values={signupfields[name].values} + type={signupFields[name].type} + values={signupFields[name].values} disabled={isBusy} /> @@ -207,7 +255,7 @@ const SignupPage = () => { type="password" label={ data.password.value.length === 0 - ? signupfields.password.label + ? signupFields.password.label : "" } value={data.password.value} @@ -225,7 +273,7 @@ const SignupPage = () => { type="password" label={ data.confirmPassword.value.length === 0 - ? signupfields.confirmPassword.label + ? signupFields.confirmPassword.label : "" } value={data.confirmPassword.value} diff --git a/src/Constants/Fields/PaymentFields.js b/src/Constants/Fields/PaymentFields.js index d59546c..0066084 100644 --- a/src/Constants/Fields/PaymentFields.js +++ b/src/Constants/Fields/PaymentFields.js @@ -23,4 +23,16 @@ export const paymentfields = { maxLength: 5, type: "cardCvvCode", }, + cardExpirationMonth: { + required: true, + label: "Card Expiration Month", + maxLength: 2, + type: "cardExpirationMonth", + }, + cardExpirationYear: { + required: true, + label: "Card Expiration Year", + maxLength: 4, + type: "cardExpirationYear", + }, }; diff --git a/src/Constants/Fields/Signupfields.js b/src/Constants/Fields/SignupFields.js similarity index 97% rename from src/Constants/Fields/Signupfields.js rename to src/Constants/Fields/SignupFields.js index 69a4faf..4eeb216 100644 --- a/src/Constants/Fields/Signupfields.js +++ b/src/Constants/Fields/SignupFields.js @@ -1,4 +1,4 @@ -export const signupfields = { +export const signupFields = { firstName: { required: true, label: "First Name", maxLength: 45 }, lastName: { required: true, label: "Last Name", maxLength: 45 }, firm: { required: true, label: "Firm", minLength: 2, maxLength: 45 },