diff --git a/README.md b/README.md index 4700e76..ceef325 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,26 @@ A simple **S**ingle **P**age **A**pp for getting funded. Built on Development and support provided by [Tincre](https://tincre.com). +## Features + +- Add your SAFE note, a business email, and [SendGrid](https://sendgrid.com) API key, get on-demand +email delivery of your note for authenticated users plus a notification to your business email. + +- Simple content changes maximize efficient customization for your business and your capital raise. + +- [Tailwindcss](https://tailwindcss.com) styling to beautifully modify your css right next to your +logic. + +- [Next.js](https://nextjs.org) to optimize your engineering on the web. + +- [next-auth](https://next-auth.js.org) for build-in security over your walled garden + SEC requirements for **not** marketing private placements (SAFE) to non-accredited investors. + +- next-seo for easy and efficient Search Engine Optimization. + +- jest/React Testing Library to declaratively test components and page renders. + +- Sensible defaults! + ## Development Setup Clone this repo. Then, diff --git a/components/Modal.jsx b/components/Modal.jsx index d82102f..d801c3c 100644 --- a/components/Modal.jsx +++ b/components/Modal.jsx @@ -2,8 +2,28 @@ import { Fragment, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { CheckIcon } from "@heroicons/react/outline"; -export default function Modal({ title, buttonText, setIsClicked, children }) { +export default function Modal({ + title, + buttonText, + setIsClicked, + safeType, + children, +}) { const [open, setOpen] = useState(true); + const emailMe = async (e, safeType) => { + try { + e.preventDefault(); + fetch("/api/email-me", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + data: safeType, + }); + } catch (error) { + console.error(`Something went wrong during email attempt. ${error}`); + } + }; return ( @@ -63,16 +83,19 @@ export default function Modal({ title, buttonText, setIsClicked, children }) {
- +
+ +
diff --git a/components/Sections/SafeTerms.jsx b/components/Sections/SafeTerms.jsx index d971276..d18cdbf 100644 --- a/components/Sections/SafeTerms.jsx +++ b/components/Sections/SafeTerms.jsx @@ -75,7 +75,7 @@ export default function SafeTerms({ ) : ( - + )} diff --git a/lib/node-utils.js b/lib/node-utils.js index 78cf7d4..b19e25d 100644 --- a/lib/node-utils.js +++ b/lib/node-utils.js @@ -1,4 +1,8 @@ import jwt from "jsonwebtoken"; +import sgMail from '@sendgrid/mail'; + +sgMail.setApiKey(process.env.EMAIL_API_KEY); + /** * Encode (and sign) a given object as JWT. * @@ -49,5 +53,46 @@ function getCsp(isProd) { csp += `connect-src 'self' https://vitals.vercel-insights.com`; return csp; } - -export { getCsp, logApiError, encodeJwt }; +/** + * A simple resolver for the safe note file name + * + * @param safeType string "Cap", "Discount", or "MFN" + * @returns string || null + */ +function resolveSafeTypeToFilename(safeType) { + const lower = safeType.toLowerCase(); + if (lower.includes('cap')) { + return 'PostmoneySafe-ValuationCapOnlyv1.1-cb4934724e32a5864fd43ca894e51c8c081ecef136409c2e94f00d92168230ec.pdf'; + } + if (lower.includes('discount')) { + return 'PostmoneySafe-DiscountOnlyv1.1-8b5fdc03d26538ff24b3336d7a3e8f2b6756eca527a78b1c64fe0dc73904ce3b.pdf'; + } + if (lower.includes('mfn')) { + return 'PostmoneySafe-MFNOnlyv1.2-53dee6e87d480c724786fbe5339fa27871989b493bb3a0f73f8c54a953f040bb.pdf'; + } + return null; +} +/* + * A function to email a notification to business development. + * + * @param string toEmail + * @param string notificationMessage + * + * @returns Boolean true if the message was sent, false otherwise. + */ +async function sendEmailNotification(toEmail, notificationMessage) { + try { + const msg = { + to: toEmail, + from: process.env.FROM_EMAIL, + subject: `Notification from Phund`, + text: notificationMessage, + }; + await sgMail.send(msg); + return true; + } catch (error) { + console.error(error); + return false; + } +} +export { getCsp, logApiError, encodeJwt, resolveSafeTypeToFilename, sendEmailNotification}; diff --git a/package.json b/package.json index 8504ef5..a60e774 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@headlessui/react": "^1.4.3", "@heroicons/react": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.1", + "@sendgrid/mail": "^7.6.0", "@tailwindcss/typography": "^0.5.1", "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", diff --git a/pages/api/email-me.js b/pages/api/email-me.js new file mode 100644 index 0000000..3e757b4 --- /dev/null +++ b/pages/api/email-me.js @@ -0,0 +1,55 @@ +/* eslint-disable import/no-anonymous-default-export */ +import sgMail from "@sendgrid/mail"; +import fs from "fs"; +import {getSession} from "next-auth/react"; +import path from "path"; +import {resolveSafeTypeToFilename, sendEmailNotification} from '../../lib/node-utils'; + +sgMail.setApiKey(process.env.EMAIL_API_KEY); +const businessUserEmail = 'jason@tincre.com' +const notificationMessage = 'The following user emailed themselves our SAFE note. You should probably follow up tiger. ;-)\n\n'; +export default async (req, res) => { + if (req.method === "POST") { + try { + const session = await getSession({req}); + if (!session) { + return res.status(403).json({error : "Not authorized"}); + } + const safeType = process.env.SAFE_TYPE; + const filename = resolveSafeTypeToFilename(safeType); + const dirRelativeToPublicFolder = "safes"; + + const dir = path.resolve("./public", dirRelativeToPublicFolder); + const pathToAttachment = `${dir}/${filename}`; + const attachment = fs.readFileSync(pathToAttachment).toString("base64"); + const message = "Hi!\n\nWe're thrilled that you're interested in investing to help us continue our growth. We will reach out shortly.\n\nMeanwhile, please find your requested SAFE note attached."; + const msg = { + to : session?.user?.email, + from : process.env.FROM_EMAIL, + subject : `Your requested SAFE note.`, + text : message, + attachments : [ + { + content : attachment, + filename : filename, + type : "application/pdf", + disposition : "attachment", + }, + ], + }; + await sgMail.send(msg); + const notificatWasSent = await sendEmailNotification(businessUserEmail, notificationMessage + ` - Investor email: ${session?.user?.email}`); + if (notificatWasSent) { + res.status(200).json({message: `Email has been sent and the business notified.`}); + + } + else { + res.status(200).json({message: `Email has been sent`}); + } + } catch (error) { + console.error(error); + res.status(500).json({error : "Error sending email"}); + } + } + res.end(); +}; diff --git a/public/safes/PostmoneySafe-DiscountOnlyv1.1-8b5fdc03d26538ff24b3336d7a3e8f2b6756eca527a78b1c64fe0dc73904ce3b.pdf b/public/safes/PostmoneySafe-DiscountOnlyv1.1-8b5fdc03d26538ff24b3336d7a3e8f2b6756eca527a78b1c64fe0dc73904ce3b.pdf new file mode 100644 index 0000000..d2bfc7e Binary files /dev/null and b/public/safes/PostmoneySafe-DiscountOnlyv1.1-8b5fdc03d26538ff24b3336d7a3e8f2b6756eca527a78b1c64fe0dc73904ce3b.pdf differ diff --git a/public/safes/PostmoneySafe-MFNOnlyv1.2-53dee6e87d480c724786fbe5339fa27871989b493bb3a0f73f8c54a953f040bb.pdf b/public/safes/PostmoneySafe-MFNOnlyv1.2-53dee6e87d480c724786fbe5339fa27871989b493bb3a0f73f8c54a953f040bb.pdf new file mode 100644 index 0000000..7bfc7f8 Binary files /dev/null and b/public/safes/PostmoneySafe-MFNOnlyv1.2-53dee6e87d480c724786fbe5339fa27871989b493bb3a0f73f8c54a953f040bb.pdf differ diff --git a/public/safes/PostmoneySafe-ValuationCapOnlyv1.1-cb4934724e32a5864fd43ca894e51c8c081ecef136409c2e94f00d92168230ec.pdf b/public/safes/PostmoneySafe-ValuationCapOnlyv1.1-cb4934724e32a5864fd43ca894e51c8c081ecef136409c2e94f00d92168230ec.pdf new file mode 100644 index 0000000..2e54b4c Binary files /dev/null and b/public/safes/PostmoneySafe-ValuationCapOnlyv1.1-cb4934724e32a5864fd43ca894e51c8c081ecef136409c2e94f00d92168230ec.pdf differ diff --git a/yarn.lock b/yarn.lock index ef2eead..edc0995 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,6 +645,29 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== +"@sendgrid/client@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.6.0.tgz#f90cb8759c96e1d90224f29ad98f8fdc2be287f3" + integrity sha512-cpBVZKLlMTO+vpE18krTixubYmZa98oTbLkqBDuTiA3zRkW+urrxg7pDR24TkI35Mid0Zru8jDHwnOiqrXu0TA== + dependencies: + "@sendgrid/helpers" "^7.6.0" + axios "^0.21.4" + +"@sendgrid/helpers@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-7.6.0.tgz#b381bfab391bcd66c771811b22bb6bb2d5c1dfc6" + integrity sha512-0uWD+HSXLl4Z/X3cN+UMQC20RE7xwAACgppnfjDyvKG0KvJcUgDGz7HDdQkiMUdcVWfmyk6zKSg7XKfKzBjTwA== + dependencies: + deepmerge "^4.2.2" + +"@sendgrid/mail@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-7.6.0.tgz#e74ee30110527feab5d3b83d68af0cd94537f6d2" + integrity sha512-0KdaSZzflJD/vUAZjB3ALBIuaVGoLq22hrb2fvQXZHRepU/yhRNlEOqrr05MfKBnKskzq1blnD1J0fHxiwaolw== + dependencies: + "@sendgrid/client" "^7.6.0" + "@sendgrid/helpers" "^7.6.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1102,6 +1125,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -2037,6 +2067,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +follow-redirects@^1.14.0: + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"