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 = () => {
- {"" !== 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 ? (
+ <>
+
+
+
+
+
+
+ {" "}
+ {`${description}`}{" "}
+
+
{`${details}`}
+
+
+ >
+ ) : (
+ <>
+
+
+
+
{`${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;
+}