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/Account/AccountPage.js b/src/Components/Account/AccountPage.js index 49613f5..d88f85e 100644 --- a/src/Components/Account/AccountPage.js +++ b/src/Components/Account/AccountPage.js @@ -17,6 +17,12 @@ const AccountPage = () => { const [docsAllowed, setDocsAllowed] = useState(10); const appUserId = group ? group.appUserId : null; const [showModal, setShowModal] = useState(false); + + const apiUrl = + process.env.NODE_ENV === "development" + ? process.env.REACT_APP_API_DEV + : process.env.REACT_APP_API_PROD; + function getDocumentsCount() { if (!appUserId) { return; @@ -37,10 +43,18 @@ const AccountPage = () => { return null; } - const handleCancelSub = () => { + const handleCancelSub = async () => { setShowModal(true); try { - //API call(s) + let response = await fetch(`${apiUrl}/cancel-subscription`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + appUserId: appUserId, + }), + }) } catch (err) { console.log(err); } diff --git a/src/Components/SignupPage/SignupPage.js b/src/Components/SignupPage/SignupPage.js index 8328b3a..52100b0 100644 --- a/src/Components/SignupPage/SignupPage.js +++ b/src/Components/SignupPage/SignupPage.js @@ -17,25 +17,40 @@ 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 { signupRadioFields } from "../../Constants/Fields/RadioFields"; import PaymentModal from "../Modals/PaymentModal"; +import Radio from "../../pageElements/Radio"; 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 [showSelectPlan, setShowSelectPlan] = useState(true); + const [activeRadioOption, setActiveRadioOption] = useState("monthlyBasic"); const modalText = "Description of monthly and annual subscriptions"; + const [selectedPlan, setSelectedPlan] = useState({}); + // 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); } @@ -53,8 +68,22 @@ const SignupPage = () => { } }; + const handleChangeRadioInput = (value) => { + const tempPlan = signupRadioFields.filter((field) => { + return field.value === value; + }); + console.log("tempPlan", tempPlan); + setSelectedPlan(tempPlan); + const temp = value; + setActiveRadioOption(temp); + }; + + signupRadioFields.map((option) => { + console.log(option.value); + }); + 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); @@ -85,6 +114,9 @@ const SignupPage = () => { email, } = dataValues; + const subscriptionId = dataValues?.subscriptionId; + const customerId = dataValues?.customerId; + const userData = { appUserId, fbAuthUid, @@ -100,6 +132,8 @@ const SignupPage = () => { barNumber, practiceArea, email, + subscriptionId, + customerId, }; try { @@ -113,6 +147,11 @@ const SignupPage = () => { const handleProceedToPayment = (e) => { e.preventDefault(); + setShowPaymentModal(true); + }; + + const handleOpenSelectPlan = (e) => { + e.preventDefault(); if (isBusy) { return; } @@ -121,14 +160,69 @@ const SignupPage = () => { if (dataValues === null) { return; } - - setShowPaymentModal(true); }; + async function handleStripeAuthorization( + user, + paymentDataValues, + customerDataValues + ) { + let error = false; + let subscriptionId = null; + let customerId = null; - async function handleStripeAuthorization(paymentDataValues) { - // Do: calls to Stripe API - // if success, return some truthy value or - // handle failure in handleSignup + 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, + }), + }); + + response = await response.json(); + + subscriptionId = response.subscriptionId; + customerId = response.customerId; + + console.log("response", response); + + return { + subscriptionId, + customerId, + }; + } catch (error) { + console.log(error); + error = true; + } + + if (error) { + return false; + } + return { + subscriptionId, + customerId, + }; } const handleSignup = async (e) => { @@ -138,14 +232,14 @@ const SignupPage = () => { return; } - const dataValues = validateData(); + let dataValues = validateData(); if (dataValues === null) { return; } setIsBusy(true); - const paymentSuccess = handleStripeAuthorization(paymentDataValues); - if (paymentSuccess) { + + if (paymentDataValues && dataValues) { try { const userCredential = await createUserWithEmailAndPassword( auth, @@ -153,19 +247,35 @@ const SignupPage = () => { dataValues.password ); const user = userCredential.user; - await saveUserData(user.uid, dataValues); - navigate("/"); + + const paymentResponse = await handleStripeAuthorization( + user, + paymentDataValues, + dataValues + ); + + if (paymentResponse === false) { + // handle payment failure + setIsBusy(false); + setNotice("Sorry, something went wrong. Please try again."); + return; + } else { + dataValues = { ...dataValues, ...paymentResponse }; + + await saveUserData(user.uid, dataValues); + navigate("/"); + } } catch (error) { setIsBusy(false); setNotice( error.message || "Sorry, something went wrong. Please try again." ); } - } else { - //handle payment failure } }; + const showNoticeOnPage = "" !== notice && !showPaymentModal; + return (
@@ -174,7 +284,7 @@ const SignupPage = () => {
{splitEvery( - Object.keys(signupfields).slice(0, -2), + Object.keys(signupFields).slice(0, -2), fieldsChunkSize ).map((names, j) => ( @@ -188,25 +298,25 @@ 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} /> ))} ))} - + { /> - + { className="primary-button signup-proceed" type="button" size="lg" - onClick={handleProceedToPayment} + onClick={handleOpenSelectPlan} disabled={isBusy} - labelText="Proceed to Payment" + labelText="Select A Plan" /> -
- - Go to login - -
- {"" !== notice && ( + {showNoticeOnPage ? ( <>

{notice}
+ ) : ( + <> )} + {showSelectPlan ? ( +
+
+
+
+ Select your subscription plan +
+
+
+ {signupRadioFields?.map((option, i) => ( + + ))} +
+
+
+
+
Order summary
+
{`{subVar} subscription type`}
+
{`{subVar} subscription mini description`}
+
{`Total billed today: {subVar} `}
+
Plus aplicable sales tax -toooltip
+
+
+
+
+
+ ) : ( + <> + )} {showPaymentModal ? ( { handleChangePaymentInput={handleChangePaymentInput} handleSignup={handleSignup} modalText={modalText} + notice={notice} + setNotice={setNotice} /> ) : ( <> 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/RadioFields.js b/src/Constants/Fields/RadioFields.js new file mode 100644 index 0000000..ea9ffcb --- /dev/null +++ b/src/Constants/Fields/RadioFields.js @@ -0,0 +1,26 @@ +export const signupRadioFields = [ + { + name: "Monthly - basic", + label: "Monthly - basic", + value: "monthlyBasic", + description: "Generate 1 document per month", + details: "Billed every month", + price: 89, + }, + { + name: "Monthly - pro", + label: "Monthly - pro", + value: "monthlyProPlan", + description: "Generate up to 3 documents per month", + details: "Billed every three months. A xxx percent savings", + price: 159, + }, + { + name: "Annual - unlimted annual", + label: "Annual - unlimited annual", + value: "annualUnlimited", + description: "Generate unlimited documents.", + details: "Billed annualy. A xxx percent savings", + price: 1489, + }, +]; diff --git a/src/Constants/Fields/Signupfields.js b/src/Constants/Fields/SignupFields.js similarity index 88% rename from src/Constants/Fields/Signupfields.js rename to src/Constants/Fields/SignupFields.js index 69a4faf..0c9d58d 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 }, @@ -31,7 +31,13 @@ export const signupfields = { }, barNumber: { required: true, label: "Bar Number", maxLength: 45 }, practiceArea: { required: true, label: "Practice Area", maxLength: 45 }, - email: { required: true, label: "Email", maxLength: 45, type: "email" }, + email: { + required: true, + label: "Email", + maxLength: 45, + type: "email", + inputWidth: "80%", + }, password: { required: true, label: "Password", diff --git a/src/Utils/Form.js b/src/Utils/Form.js index 0ac9d9e..c719043 100644 --- a/src/Utils/Form.js +++ b/src/Utils/Form.js @@ -84,6 +84,11 @@ export const getValidatedFormData = (data, fields) => }, data); export const handleFormDataChange = (e, name, data, fields) => { + console.log("fields", fields); + if (!fields) { + return; + } + const value = (typeof e?.target?.value === "undefined" ? e.value : e.target.value) || ""; const field = fields[name]; diff --git a/src/pageElements/Radio.js b/src/pageElements/Radio.js index e4313cb..20b3e5e 100644 --- a/src/pageElements/Radio.js +++ b/src/pageElements/Radio.js @@ -1,23 +1,73 @@ +import { CircleFill, Circle } from "react-bootstrap-icons"; +import "../styles/radio.scss"; -const Radio = ({ - rightLabel, - leftLabel, - containerClassName, - labelClassName, - ...props -}) => { +const Radio = (props) => { + console.log("hello"); + const { + name, + description, + onClick, + value, + price, + disabled, + activeRadioOption, + details, + } = props; + const classCheckbox = "checkbox"; + const classOption = "option"; + console.log("value, activeRadioOption", value, activeRadioOption); return ( -
- +
+
!disabled && onClick && onClick(value)} + > + {value == activeRadioOption ? ( + <> +
+
+
+ +
+
{`${name}`}
+
+
{`${price}`}
+
+
+ +
+
+
+ {" "} + {`${description}`}{" "} +
+
{`${details}`}
+
+
+ + ) : ( + <> +
+
+
+ +
+
{`${name}`}
+
+
{`${price}`}
+
+
+
+
{`${description}`}
+
{`${details}`}
+
+
+ + )} +
); }; export default Radio; - -// diff --git a/src/pageElements/Radiogroup.js b/src/pageElements/Radiogroup.js index a05a790..1ecd057 100644 --- a/src/pageElements/Radiogroup.js +++ b/src/pageElements/Radiogroup.js @@ -11,7 +11,7 @@ const Radiogroup = (props) => { onClick, value, options, - disabled + disabled, } = props; const classCheckbox = "checkbox"; const classOption = "option"; @@ -90,7 +90,3 @@ const Radiogroup = (props) => { */ export default Radiogroup; - -/* - - */ diff --git a/src/styles/radio.scss b/src/styles/radio.scss new file mode 100644 index 0000000..a99f924 --- /dev/null +++ b/src/styles/radio.scss @@ -0,0 +1,49 @@ +.radio-circle-container { + display: flex; + flex-direction: column; + background-color: #fff; + height: 160px; + margin: 10px 8px 0px 8px; + border-radius: 5px; + border: 1px solid #d4d4d4; +} + +.radio-circle-top { + height: 40px; + display: flex; + flex-direction: row; +} + +.radio-circle-lower { + display: flex; + flex-direction: column; + margin-left: 18px; + padding-left: 26px; +} + +.radio-circle-box { + height: 40px; + margin: 12px 8px; +} + +.radio-name-box { + height: 40px; + width: 50%; + font-family: var(--main-font); + font-family: Roboto; + font-weight: 500; + letter-spacing: 0.05rem; + margin: 13px 8px; +} + +.radio-price-box { + display: flex; + flex-direction: row-reverse; + margin: 16px 8px; + width: 30%; +} + +.radio-price { + width: 20px; + float: right; +} diff --git a/src/styles/signup.scss b/src/styles/signup.scss index 5fbd148..941400a 100644 --- a/src/styles/signup.scss +++ b/src/styles/signup.scss @@ -32,12 +32,21 @@ font-size: 2rem; } +.signup-subheader-text { + letter-spacing: -0.6px; + font-weight: 400; + font-size: 1.7rem; + margin-left: 5px; + margin-top: 6px; +} + .signup-form { width: 880px; } .signup-button-box { width: 100%; + margin-bottom: 24px; display: flex; flex-direction: column; align-items: flex-end; @@ -46,3 +55,70 @@ .signup-proceed { width: 160px; } + +.select-plan-container { + display: flex; + flex-direction: row; + margin-top: 24px; + width: 920px; + height: 600px; +} + +.select-sub-left { + width: 60%; + border-radius: 10px; + background-color: rgb(240, 247, 250); + margin-right: 8px; + border: 1px solid #e9e9e9; + padding: 6px; +} + +.select-sub-right { + width: 40%; + border-radius: 10px; + background-color: rgb(240, 247, 250); + margin-left: 8px; + border: 1px solid #e9e9e9; + padding: 6px; +} + +.signup-radio-wrapper { + background-color: #fff; + display: flex; + flex-direction: column; +} + +.signup-option-div { + display: flex; + flex-direction: column; +} + +.radio-group-box { + display: flex; + flex-direction: column; + padding: 6px; + width: 100%; + height: 300px; +} + +.select-plan-summary-box { + display: flex; + flex-direction: column; + height: 250px; + width: 100%; + border-radius: 10px; + background-color: #fff; + border: 1px solid #e9e9e9; + padding: 6px; +} + +.selectplan-payment-button-wrapper { + display: flex; + flex-direction: row-reverse; + margin: 6px; + padding: 6px; +} + +.signup-option { + width: 100% !important; +}