├── .gitignore
├── README.md
├── client
├── .env
├── .env-sample
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── HomePage.js
│ ├── account
│ ├── AccountActivation.js
│ ├── Login.js
│ ├── Logout.js
│ ├── Register.js
│ ├── ResendActivationLink.js
│ ├── ResetPassword.js
│ ├── ResetPasswordLink.js
│ ├── account.scss
│ ├── actionsAuth.js
│ └── reducerAuth.js
│ ├── common
│ └── Errors.js
│ ├── header
│ └── Header.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── payments
│ ├── CreateSubscription.js
│ ├── CreditCardForm.js
│ ├── CurrentCard.js
│ ├── CurrentSubscription.js
│ ├── Payments.js
│ ├── StripeProviderComponent.js
│ └── UpdateCard.js
│ ├── serviceWorker.js
│ └── store
│ ├── configureStore.js
│ └── rootReducer.js
├── client2
├── .env
├── .env-sample
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
│ ├── App.js
│ ├── App.scss
│ ├── App.test.js
│ ├── HomePage.js
│ ├── account
│ ├── AccountActivation.js
│ ├── Login.js
│ ├── Logout.js
│ ├── Register.js
│ ├── ResendActivationLink.js
│ ├── ResetPassword.js
│ ├── ResetPasswordLink.js
│ ├── account.scss
│ ├── actionsAuth.js
│ └── reducerAuth.js
│ ├── common
│ └── Errors.js
│ ├── header
│ └── Header.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── payments
│ ├── CreateSubscription.js
│ ├── CreditCardForm.js
│ ├── CurrentCard.js
│ ├── CurrentSubscription.js
│ ├── Payments.js
│ ├── StripeProviderComponent.js
│ └── UpdateCard.js
│ ├── serviceWorker.js
│ └── store
│ ├── configureStore.js
│ └── rootReducer.js
├── package.json
├── server
├── .env-sample
├── controllers
│ ├── authController.js
│ └── paymentsController.js
├── db
│ └── databaseSetup.js
├── lib
│ ├── EmailManager.js
│ ├── StripeManager.js
│ └── passport.js
├── models
│ └── User.js
├── package-lock.json
├── package.json
├── routes.js
└── server.js
└── xscreenshots
├── 1.HomePage.png
├── 2.Register.png
├── 3.Login.png
├── 4.ResendActivationLink.png
├── 5.ResetPasswordLink.png
├── 6.ResetPassword.png
└── 7.Payments.png
/.gitignore:
--------------------------------------------------------------------------------
1 | server/node_modules/
2 | node_modules/
3 | server/.env
4 |
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SAASiFy
2 |
3 | SAASiFy is a sample SAAS app built with NodeJS and ReactJS. It is primarily built for the Udemy course [SaasiFy - Build a complete SAAS App this weekend](https://www.udemy.com/course/2786970).
4 |
5 | The commits are ordered in the same order as the videos in the course.
6 |
7 |
8 | ## Getting Started
9 |
10 | 1. Fork or clone the repository
11 | 2. You will get 2 folders -> `client` and `server`.
12 | 3. Run `npm install` from each of these folders.
13 | 4. Copy the provided `server/.env.sample` file and create a new file `server/.env`. Populate this with your values.
14 | 4. Start `client` with `npm start` inside `client` folder.
15 | 5. Start `server` with `npm start` inside `server` folder.
16 |
17 |
18 |
19 | ## Demo of all the APIs using POSTMAN
20 | The collection is available at [https://www.getpostman.com/collections/7acbd32bce8a734cdb7d](https://www.getpostman.com/collections/7acbd32bce8a734cdb7d)
21 |
22 | [](https://www.youtube.com/watch?v=w22HbHNxhv0)
23 |
24 | ## Features/Screenshots
25 |
26 | ### Home Page
27 |
28 | 
29 |
30 | ### Registration System
31 |
32 | 1. User Registers for the App
33 | 2. Send email for email address verification
34 | 3. Resend email when users don't receive it
35 | 4. Activate Account when email link is clicked
36 |
37 | 
38 |
39 | ### Authentication System
40 |
41 | 1. User login
42 | 2. JWT token based auth
43 |
44 | 
45 |
46 | ### Password System
47 |
48 | 1. Send email for Forgot Password
49 | 2. Allow password reset after clicking on email link
50 |
51 | 
52 | 
53 | 
54 |
55 | ### Payments
56 |
57 | 1. Integrate with Stripe
58 | 2. Subscriptions - Subscribe to new plan
59 | 3. Subscriptions - Upgrade or Downgrade existing plan
60 | 4. Subscriptions - Unsubscribe(Delete) from all plans
61 | 5. Credit Cards - Save card to account
62 | 6. Credit Cards - Update saved card to account
63 |
64 | 
65 |
66 |
67 | ### Email Integration
68 |
69 | 1. Mailgun API Integration
70 | 2. Learn to create, edit and send HTML template based emails
71 | 3. Use knowledge to send any email
72 |
73 | ### Postman APIs
74 |
75 | 1. Learn to test APIs using POSTMAN
76 | 2. Run whole application without opening browser
77 | 3. API collection provided
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | .
--------------------------------------------------------------------------------
/client/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_cPAYuvk1DKJ0cqm0cm0Zblms
2 |
--------------------------------------------------------------------------------
/client/.env-sample:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE
2 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^4.4.0",
7 | "js-cookie": "^2.2.1",
8 | "lodash": "^4.17.15",
9 | "node-sass": "^4.13.0",
10 | "query-string": "^6.9.0",
11 | "react": "^16.12.0",
12 | "react-bootstrap": "^1.0.0-beta.16",
13 | "react-dom": "^16.12.0",
14 | "react-notifications": "^1.4.3",
15 | "react-redux": "^7.2.0",
16 | "react-router-dom": "^5.1.2",
17 | "react-scripts": "3.2.0",
18 | "react-stripe-elements": "^6.0.1",
19 | "redux": "^4.0.5",
20 | "redux-logger": "^3.0.6",
21 | "redux-thunk": "^2.3.0"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "proxy": "http://localhost:3001",
45 | "engines": {
46 | "node": ">=10.0.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 | SaasiFy
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client/public/logo512.png
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Route
4 | } from 'react-router-dom';
5 | import HomePage from './HomePage';
6 | import Register from './account/Register';
7 | import AccountActivation from './account/AccountActivation';
8 | import ResendActivationLink from './account/ResendActivationLink';
9 | import ResetPasswordLink from './account/ResetPasswordLink';
10 | import ResetPassword from './account/ResetPassword';
11 | import Login from './account/Login';
12 | import Logout from './account/Logout';
13 | import Header from './header/Header'
14 | import Payments from './payments/Payments';
15 | import { NotificationContainer } from 'react-notifications';
16 | import { connect } from 'react-redux';
17 | import { bindActionCreators } from 'redux';
18 | import { fetchLoggedInUser } from './account/actionsAuth';
19 | import 'bootstrap/dist/css/bootstrap.min.css';
20 | import 'react-notifications/dist/react-notifications.css';
21 | import './App.scss';
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return bindActionCreators({
25 | fetchLoggedInUser
26 | }, dispatch);
27 | };
28 |
29 | class App extends React.Component {
30 | state = {
31 | user: null
32 | }
33 |
34 | componentDidMount() {
35 | this.props.fetchLoggedInUser();
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
}
58 | />
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export default connect(null, mapDispatchToProps)(App);
66 |
--------------------------------------------------------------------------------
/client/src/App.scss:
--------------------------------------------------------------------------------
1 | .app-container {
2 | padding-top: 16px;
3 | }
4 |
5 | body {
6 | background: #E3E8EE;
7 | font-family: 'Rubik', sans-serif;
8 | }
--------------------------------------------------------------------------------
/client/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router';
3 | import ListGroup from 'react-bootstrap/ListGroup';
4 | import { Link } from 'react-router-dom';
5 | import { useSelector } from 'react-redux';
6 | import _get from 'lodash/get';
7 |
8 | function HomePage() {
9 | const auth = useSelector(state => state.auth);
10 | const email = _get(auth, 'user.email');
11 |
12 | return (
13 |
14 |
{email ? `Welcome ${email}` : 'Welcome Home'}
15 |
16 |
17 | Register
18 |
19 |
20 | Login
21 |
22 |
23 | Logout
24 |
25 |
26 | Activate
27 |
28 |
29 | Resend Activation Link
30 |
31 |
32 | Reset Password Link
33 |
34 |
35 | Reset Password
36 |
37 |
38 | Payments
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default withRouter(HomePage);
47 |
--------------------------------------------------------------------------------
/client/src/account/AccountActivation.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import queryString from 'query-string';
3 | import Alert from 'react-bootstrap/Alert';
4 | import { Link } from 'react-router-dom';
5 |
6 | export default function AccountActivation(props) {
7 | const [busy, setBusy] = useState(false);
8 | const [apiState, setApiState] = useState({
9 | success: false,
10 | error: false,
11 | errors: []
12 | })
13 | console.log('props', props);
14 |
15 | useEffect(() => {
16 | // Get token from URL
17 | const queryStringParams = queryString.parse(props.location.search) || {};
18 | const token = queryStringParams.token;
19 |
20 | if (!token) {
21 | setBusy(false);
22 | setApiState({
23 | success: false,
24 | error: true,
25 | errors: []
26 | })
27 |
28 | return;
29 |
30 | }
31 |
32 | setBusy(true);
33 |
34 | const url = '/api/account-activate';
35 |
36 | fetch(url, {
37 | headers: {
38 | 'Accept': 'application/json',
39 | 'Content-Type': 'application/json'
40 | },
41 | credentials: 'same-origin',
42 | method: 'POST',
43 | body: JSON.stringify({
44 | activationToken: token
45 | })
46 | }).then((response) => {
47 | setBusy(false);
48 | if (!response.ok) {
49 | return response.json().then(err => { throw err })
50 | }
51 | return response.json();
52 | }).then((results) => {
53 | setApiState({
54 | success: true,
55 | error: false,
56 | errors: []
57 | })
58 | console.log('results ', results);
59 |
60 | }).catch((error) => {
61 | setApiState({
62 | success: false,
63 | error: true,
64 | errors: error.errors
65 | })
66 | console.log('error ', error);
67 | })
68 | }, []);
69 |
70 | const {
71 | success,
72 | error
73 | } = apiState;
74 |
75 | return (
76 |
77 | {
78 | busy ?
79 |
Activating Account ....
:
80 | null
81 | }
82 | {
83 | error &&
84 | (
85 |
86 |
87 | An error occurred. Perhaps you requested a new token?
88 |
89 |
90 | Reuqest a new Activation Token
91 |
92 |
93 |
94 | )
95 |
96 | }
97 |
98 | {
99 | success &&
100 | (
101 |
102 |
103 | Successfully activated your account. Please proceed to the Login page to Sign In
104 |
105 |
106 | Login
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
115 |
116 | )
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/client/src/account/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import Cookies from 'js-cookie';
5 | import { Link } from 'react-router-dom';
6 | import { NotificationManager } from 'react-notifications';
7 | import Errors from '../common/Errors';
8 | import './account.scss';
9 |
10 | export default function Login(props) {
11 | const [email, setEmail] = useState('');
12 | const [password, setPassword] = useState('');
13 | const [apiState, setApiState] = useState({
14 | success: false,
15 | error: false,
16 | errors: []
17 | })
18 |
19 | const handleEmailChange = (event) => {
20 | setEmail(event.target.value);
21 | }
22 |
23 | const handlePasswordChange = (event) => {
24 | setPassword(event.target.value)
25 | }
26 |
27 | const handleSubmitClick = () => {
28 | setApiState({
29 | success: false,
30 | error: false,
31 | errors: null
32 | })
33 |
34 | // Submit the email and password to the server
35 | const url = '/api/login';
36 |
37 | fetch(url, {
38 | headers: {
39 | 'Accept': 'application/json',
40 | 'Content-Type': 'application/json'
41 | },
42 | credentials: 'same-origin',
43 | method: 'POST',
44 | body: JSON.stringify({
45 | email: email,
46 | password: password
47 | })
48 | }).then((response) => {
49 | if (!response.ok) {
50 | return response.json().then(err => { throw err })
51 | }
52 | return response.json();
53 | }).then((results) => {
54 | console.log('results ', results);
55 | const token = results.token;
56 |
57 | Cookies.set('token', token, {
58 | expires: 7
59 | })
60 | NotificationManager.success('You have successfully logged in', 'Login Success');
61 | setTimeout(() => {
62 | window.location.href = '/';
63 | }, 2000);
64 | }).catch((error) => {
65 | console.log('error ', error);
66 | setApiState({
67 | success: false,
68 | error: true,
69 | errors: error.errors
70 | });
71 | })
72 | }
73 |
74 | const {
75 | errors
76 | } = apiState;
77 |
78 | return (
79 |
80 |
81 |
Login
82 |
83 |
88 | Email address
89 |
95 |
96 | We'll never share your email with anyone else.
97 |
98 |
99 |
100 |
101 | Password
102 |
108 |
109 |
116 |
117 |
118 |
119 |
120 |
Don't have an account?
121 |
Register
122 |
123 |
124 |
Trouble Signing Up?
125 |
Reset Password
126 |
127 |
128 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/client/src/account/Logout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Cookies from 'js-cookie';
3 |
4 | export default function Logout(props) {
5 | const [isLoggedOut, setIsLoggedOut] = useState(false);
6 |
7 | useEffect(() => {
8 | Cookies.remove('token');
9 | setIsLoggedOut(true);
10 | }, []);
11 |
12 | return (
13 |
14 |
15 | {
16 | isLoggedOut ?
17 |
You are logged out
:
18 |
You are logged in
19 | }
20 |
21 |
22 | );
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/account/Register.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function Register(props) {
10 | const [email, setEmail] = useState('');
11 | const [password, setPassword] = useState('');
12 | const [apiState, setApiState] = useState({
13 | success: false,
14 | error: false,
15 | errors: []
16 | })
17 |
18 | const handleEmailChange = (event) => {
19 | setEmail(event.target.value);
20 | }
21 |
22 | const handlePasswordChange = (event) => {
23 | setPassword(event.target.value)
24 | }
25 |
26 | const handleSubmitClick = () => {
27 | setApiState({
28 | success: false,
29 | error: false,
30 | errors: null
31 | })
32 |
33 | // Submit the email and password to the server
34 | const url = '/api/register';
35 |
36 | fetch(url, {
37 | headers: {
38 | 'Accept': 'application/json',
39 | 'Content-Type': 'application/json'
40 | },
41 | credentials: 'same-origin',
42 | method: 'POST',
43 | body: JSON.stringify({
44 | email,
45 | password
46 | })
47 | }).then((response) => {
48 | if (!response.ok) {
49 | return response.json().then(err => { throw err })
50 | }
51 | return response.json();
52 | }).then((results) => {
53 | console.log('results ', results);
54 | setApiState({
55 | success: true,
56 | error: false,
57 | errors: null
58 | })
59 | }).catch((error) => {
60 | console.log('error ', error);
61 | setApiState({
62 | success: false,
63 | error: true,
64 | errors: error.errors
65 | })
66 | })
67 | }
68 |
69 | const {
70 | success,
71 | errors
72 | } = apiState;
73 |
74 | return (
75 |
76 |
77 |
Register
78 |
79 |
92 | Email address
93 |
99 |
100 | We'll never share your email with anyone else.
101 |
102 |
103 |
104 |
105 | Password
106 |
112 |
113 |
120 |
121 |
122 |
123 |
Already have an account?
124 |
Login
125 |
126 |
127 |
128 | )
129 | }
130 |
--------------------------------------------------------------------------------
/client/src/account/ResendActivationLink.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function ResendActivationLink(props) {
10 | const [email, setEmail] = useState('');
11 | const [apiState, setApiState] = useState({
12 | success: false,
13 | error: false,
14 | errors: []
15 | })
16 |
17 | const handleEmailChange = (event) => {
18 | setEmail(event.target.value);
19 | }
20 |
21 | const handleSubmitClick = () => {
22 | setApiState({
23 | success: false,
24 | error: false,
25 | errors: null
26 | })
27 |
28 | // Submit the email and password to the server
29 | const url = '/api/resend-activation-link';
30 |
31 | fetch(url, {
32 | headers: {
33 | 'Accept': 'application/json',
34 | 'Content-Type': 'application/json'
35 | },
36 | credentials: 'same-origin',
37 | method: 'POST',
38 | body: JSON.stringify({
39 | email
40 | })
41 | }).then((response) => {
42 | if (!response.ok) {
43 | return response.json().then(err => { throw err })
44 | }
45 | return response.json();
46 | }).then((results) => {
47 | console.log('results ', results);
48 | setApiState({
49 | success: true,
50 | error: false,
51 | errors: null
52 | })
53 |
54 | }).catch((error) => {
55 | console.log('error ', error);
56 | setApiState({
57 | success: false,
58 | error: true,
59 | errors: error.errors
60 | })
61 |
62 | })
63 | }
64 |
65 | const {
66 | success,
67 | errors
68 | } = apiState;
69 |
70 | return (
71 |
72 |
73 |
Resend Activation Link
74 |
75 |
87 | Email address
88 |
94 |
95 | This account should already exist in our database.
96 |
97 |
98 |
99 |
106 |
107 |
108 |
109 |
110 |
111 |
Already have an account?
112 |
Login
113 |
114 |
115 |
Don't have an account?
116 |
Register
117 |
118 |
119 |
120 |
121 |
122 | )
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/client/src/account/ResetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import queryString from 'query-string';
3 | import Form from 'react-bootstrap/Form';
4 | import Button from 'react-bootstrap/Button';
5 | import { Link } from 'react-router-dom';
6 | import Alert from 'react-bootstrap/Alert';
7 | import Errors from '../common/Errors';
8 | import './account.scss';
9 |
10 | export default function ResetPassword(props) {
11 | const [password, setPassword] = useState('');
12 | const [resetPasswordToken, setResetPasswordToken] = useState('');
13 | const [apiState, setApiState] = useState({
14 | success: false,
15 | error: false,
16 | errors: []
17 | })
18 |
19 | const handlePasswordChange = (event) => {
20 | setPassword(event.target.value)
21 | }
22 |
23 | useEffect(() => {
24 | // Get token from URL
25 | const queryStringParams = queryString.parse(props.location.search) || {};
26 | const token = queryStringParams.token;
27 |
28 | if (!token) {
29 | setApiState({
30 | success: false,
31 | error: true,
32 | errors: [{
33 | message: 'You need to generate a Reset Password Token before resetting your password'
34 | }]
35 | })
36 |
37 |
38 | }
39 |
40 | setResetPasswordToken(token);
41 | }, []);
42 |
43 | const handleSubmitClick = () => {
44 | setApiState({
45 | success: false,
46 | error: false,
47 | errors: null
48 | })
49 |
50 | // Submit the email and password to the server
51 | const url = '/api/reset-password';
52 |
53 | fetch(url, {
54 | headers: {
55 | 'Accept': 'application/json',
56 | 'Content-Type': 'application/json'
57 | },
58 | credentials: 'same-origin',
59 | method: 'POST',
60 | body: JSON.stringify({
61 | resetPasswordToken: resetPasswordToken,
62 | password: password
63 | })
64 | }).then((response) => {
65 | if (!response.ok) {
66 | return response.json().then(err => { throw err })
67 | }
68 | return response.json();
69 | }).then((results) => {
70 | console.log('results ', results);
71 | setApiState({
72 | success: true,
73 | error: false,
74 | errors: null
75 | })
76 |
77 | }).catch((error) => {
78 | console.log('error ', error);
79 | setApiState({
80 | success: false,
81 | error: true,
82 | errors: error.errors
83 | })
84 | })
85 |
86 | }
87 |
88 | const {
89 | success,
90 | errors
91 | } = apiState;
92 |
93 | return (
94 |
95 |
96 |
Reset Password
97 |
98 |
110 | Password
111 |
117 |
118 | Please enter your new password
119 |
120 |
121 |
122 |
129 |
130 |
131 |
132 |
133 |
134 |
Try Login again?
135 |
Login
136 |
137 |
138 |
139 |
140 |
141 | );
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/client/src/account/ResetPasswordLink.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function ResetPasswordLink(props) {
10 | const [email, setEmail] = useState('');
11 | const [apiState, setApiState] = useState({
12 | success: false,
13 | error: false,
14 | errors: []
15 | })
16 |
17 | const handleEmailChange = (event) => {
18 | setEmail(event.target.value);
19 | }
20 |
21 | const handleSubmitClick = () => {
22 | setApiState({
23 | success: false,
24 | error: false,
25 | errors: null
26 | })
27 |
28 | // Submit the email and password to the server
29 | const url = '/api/reset-password-link';
30 |
31 | fetch(url, {
32 | headers: {
33 | 'Accept': 'application/json',
34 | 'Content-Type': 'application/json'
35 | },
36 | credentials: 'same-origin',
37 | method: 'POST',
38 | body: JSON.stringify({
39 | email
40 | })
41 | }).then((response) => {
42 | if (!response.ok) {
43 | return response.json().then(err => { throw err })
44 | }
45 | return response.json();
46 | }).then((results) => {
47 | console.log('results ', results);
48 | setApiState({
49 | success: true,
50 | error: false,
51 | errors: null
52 | })
53 |
54 | }).catch((error) => {
55 | console.log('error ', error);
56 | setApiState({
57 | success: false,
58 | error: true,
59 | errors: error.errors
60 | })
61 | })
62 | }
63 | const {
64 | success,
65 | errors
66 | } = apiState;
67 |
68 | return (
69 |
70 |
71 |
Reset Password Link
72 |
73 |
85 | Email address
86 |
92 |
93 | This account should already exist in our database.
94 |
95 |
96 |
97 |
104 |
105 |
106 |
107 |
108 |
Try Login again?
109 |
Login
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 |
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/client/src/account/account.scss:
--------------------------------------------------------------------------------
1 | .form-container {
2 | max-width: 340px;
3 | margin: 0 auto;
4 | background: white;
5 | padding: 16px;
6 | }
7 |
8 | .actions-container {
9 | margin-top: 24px;
10 | }
--------------------------------------------------------------------------------
/client/src/account/actionsAuth.js:
--------------------------------------------------------------------------------
1 | export const AUTH_BUSY = 'AUTH_BUSY';
2 | export const AUTH_RETRIEVEUSER_SUCCESS = 'AUTH_RETRIEVEUSER_SUCCESS';
3 | export const AUTH_RETRIEVEUSER_ERROR = 'AUTH_RETRIEVEUSER_ERROR';
4 |
5 | export function fetchLoggedInUser() {
6 | return function(dispatch, getState) {
7 | dispatch({
8 | type: AUTH_BUSY
9 | });
10 | const url = '/api/logged-in-user';
11 |
12 | fetch(url, {
13 | headers: {
14 | 'Accept': 'application/json',
15 | 'Content-Type': 'application/json'
16 | },
17 | credentials: 'same-origin',
18 | method: 'GET'
19 | }).then((response) => {
20 | if (!response.ok) {
21 | return response.json().then(err => { throw err })
22 | }
23 | return response.json();
24 | }).then((results) => {
25 | console.log('results ', results);
26 | const user = results.user;
27 |
28 | dispatch({
29 | type: AUTH_RETRIEVEUSER_SUCCESS,
30 | payload: user
31 | });
32 | }).catch((error) => {
33 | console.log('error ', error);
34 | dispatch({
35 | type: AUTH_RETRIEVEUSER_ERROR,
36 | payload: null
37 | });
38 | })
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/account/reducerAuth.js:
--------------------------------------------------------------------------------
1 | import _isEmpty from 'lodash/isEmpty';
2 |
3 | import {
4 | AUTH_BUSY,
5 | AUTH_RETRIEVEUSER_SUCCESS,
6 | AUTH_RETRIEVEUSER_ERROR
7 | } from './actionsAuth';
8 |
9 | export default function auth(state = {}, action) {
10 | switch(action.type) {
11 | case AUTH_BUSY: {
12 | return {
13 | ...state,
14 | busy: true
15 | }
16 | }
17 |
18 | case AUTH_RETRIEVEUSER_ERROR: {
19 | return {
20 | ...state,
21 | busy: false,
22 | error: null,
23 | authenticated: false,
24 | user: null
25 | }
26 | }
27 | case AUTH_RETRIEVEUSER_SUCCESS: {
28 | const { payload = {} } = action;
29 | return {
30 | ...state,
31 | busy: false,
32 | error: null,
33 | authenticated: !_isEmpty(payload),
34 | user: payload
35 | }
36 | }
37 | default: return state;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/common/Errors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Alert from 'react-bootstrap/Alert';
3 | import _map from 'lodash/map';
4 |
5 | export default class Errors extends React.Component {
6 | render() {
7 | const {
8 | errors
9 | } = this.props;
10 |
11 | const errorMessages = _map(errors, 'message');
12 |
13 | return (
14 |
15 | {
16 | _map(errorMessages, (error, index) => {
17 | return (
18 |
19 | {error}
20 |
21 | )
22 | })
23 | }
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Navbar from 'react-bootstrap/Navbar';
3 | import Nav from 'react-bootstrap/Nav';
4 |
5 | export class Header extends Component {
6 | render() {
7 | return (
8 |
9 | <>
10 |
11 | SAASIFY
12 |
17 |
18 | >
19 |
20 | );
21 | }
22 | }
23 |
24 | export default Header;
25 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import store from './store/configureStore';
5 | import './index.css';
6 | import App from './App';
7 | import * as serviceWorker from './serviceWorker';
8 | import {
9 | BrowserRouter as Router
10 | } from 'react-router-dom';
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 |
17 |
18 | , document.getElementById('root'));
19 |
20 | // If you want your app to work offline and load faster, you can change
21 | // unregister() to register() below. Note this comes with some pitfalls.
22 | // Learn more about service workers: https://bit.ly/CRA-PWA
23 | serviceWorker.unregister();
24 |
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/payments/CreateSubscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import StripeProviderComponent from './StripeProviderComponent';
5 | import Card from 'react-bootstrap/Card';
6 | import _get from 'lodash/get';
7 |
8 | export default class CreateSubscription extends React.Component {
9 | handlePlanChange = (event) => {
10 | this.props.setPlan(event.target.value)
11 | }
12 |
13 | createSubscription = () => {
14 | this.props.onPaymentMethodCreated();
15 | }
16 |
17 | render() {
18 | const {
19 | user
20 | } = this.props;
21 |
22 | const hasSubscription = _get(user, 'stripeDetails.subscription.status', '') === 'active';
23 |
24 | return (
25 |
26 |
27 | {
28 | hasSubscription ?
29 | Update your subscription
:
30 | Subscribe to a plan
31 | }
32 |
33 |
35 | Select your plan
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {
49 | !this.props.card &&
50 |
55 | }
56 |
57 | {
58 | this.props.card &&
59 |
66 | }
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/payments/CreditCardForm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Use the CSS tab above to style your Element's container.
3 | */
4 | import React from 'react';
5 | import { CardElement, injectStripe } from 'react-stripe-elements';
6 | import Button from 'react-bootstrap/Button';
7 |
8 | const style = {
9 | base: {
10 | color: "#32325d",
11 | fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
12 | fontSmoothing: "antialiased",
13 | fontSize: "16px",
14 | "::placeholder": {
15 | color: "#aab7c4"
16 | }
17 | },
18 | invalid: {
19 | color: "#fa755a",
20 | iconColor: "#fa755a"
21 | }
22 | };
23 |
24 | class CardSection extends React.Component {
25 | handleSubmitClick = async (event) => {
26 |
27 | const cardElement = this.props.elements.getElement('card');
28 |
29 | const { paymentMethod, error } = await this.props.stripe.createPaymentMethod({
30 | type: 'card',
31 | card: cardElement,
32 | billing_details: {
33 | email: this.props.user.email,
34 | },
35 | });
36 |
37 | console.log('paymentMethod', paymentMethod);
38 | console.log('error', error);
39 |
40 | if (!error) {
41 | this.props.onPaymentMethodCreated(paymentMethod);
42 | }
43 | }
44 |
45 | render() {
46 | return (
47 |
63 | );
64 | }
65 | };
66 |
67 | export default injectStripe(CardSection);
68 |
--------------------------------------------------------------------------------
/client/src/payments/CurrentCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _get from 'lodash/get';
3 | import Card from 'react-bootstrap/Card';
4 |
5 | export default class CurrentCard extends React.Component {
6 | render() {
7 | const {
8 | card
9 | } = this.props;
10 |
11 | const brand = _get(card, 'brand');
12 | const exp_month = _get(card, 'exp_month');
13 | const exp_year = _get(card, 'exp_year');
14 | const last4 = _get(card, 'last4');
15 |
16 | if (!card) {
17 | return (
18 |
19 |
20 | Card details
21 | You do not have any saved cards.
22 |
23 |
24 | )
25 | }
26 |
27 | return (
28 |
29 |
30 | Card details
31 |
32 |
Brand
33 |
{brand}
34 |
Expiry Month
35 |
{exp_month}
36 |
Expiry Year
37 |
{exp_year}
38 |
Last 4
39 |
{last4}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/payments/CurrentSubscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from 'react-bootstrap/Card';
3 | import Button from 'react-bootstrap/Button';
4 |
5 | export default class CurrentSubscription extends React.Component {
6 | render() {
7 | const {
8 | subscription
9 | } = this.props;
10 | if (!subscription) {
11 | return (
12 |
13 |
14 | Subscription details
15 | You do not have any subscriptions yet.
16 |
17 |
18 | )
19 | }
20 |
21 | const nickname = subscription.plan.nickname;
22 | const amount = subscription.plan.amount;
23 | const interval = subscription.plan.interval;
24 | const status = subscription.status;
25 |
26 |
27 | return (
28 |
29 |
30 | Subscription details
31 |
32 |
Status
33 |
{status}
34 |
You are subscribed to
35 |
{nickname}
36 |
We will charge you
37 |
{amount}
38 |
Interval
39 |
{interval}
40 |
41 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/payments/Payments.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CurrentSubscription from './CurrentSubscription';
3 | import CurrentCard from './CurrentCard';
4 | import CreateSubscription from './CreateSubscription';
5 | import UpdateCard from './UpdateCard';
6 | import _get from 'lodash/get';
7 |
8 | const stripe = Stripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); // eslint-disable-line
9 |
10 | export default class Payments extends React.Component {
11 | state = {
12 | subscription: null,
13 | card: null,
14 | plan: ''
15 | }
16 | componentDidMount() {
17 | this.getPaymentDetails();
18 | }
19 |
20 | getPaymentDetails = () => {
21 | this.getSubscription();
22 | this.getCard();
23 | }
24 |
25 | setPlan = (planId) => {
26 | this.setState({
27 | plan: planId
28 | })
29 | }
30 |
31 | createSubscription = (paymentMethod) => {
32 | if (!this.state.plan) {
33 | // Show error to user
34 | return;
35 | }
36 |
37 | const planId = this.state.plan;
38 |
39 | const url = '/api/payments/subscriptions';
40 |
41 | fetch(url, {
42 | headers: {
43 | 'Accept': 'application/json',
44 | 'Content-Type': 'application/json'
45 | },
46 | credentials: 'same-origin',
47 | method: 'POST',
48 | body: JSON.stringify({
49 | planId: planId,
50 | paymentMethod
51 | })
52 | }).then((response) => {
53 | if (!response.ok) {
54 | return response.json().then(err => { throw err })
55 | }
56 | return response.json();
57 | }).then((results) => {
58 | console.log('results ', results);
59 | const subscription = _get(results, 'subscription');
60 | const paymentIntentStatus = _get(subscription, 'latest_invoice.payment_intent.status', '');
61 | const client_secret = _get(subscription, 'latest_invoice.payment_intent.client_secret', '');
62 | if (paymentIntentStatus === 'requires_action' || paymentIntentStatus === 'requires_payment_method') {
63 | stripe.confirmCardPayment(client_secret).then(function(result) {
64 | console.log('confirmCardPayment result', result);
65 | if (result.error) {
66 | // Display error message in your UI.
67 | // The card was declined (i.e. insufficient funds, card has expired, etc)
68 | alert(result.error);
69 |
70 | } else {
71 | // Show a success message to your customer
72 | alert('You have successfully confirmed the payment');
73 | window.location.href = '/payments';
74 | }
75 |
76 |
77 | });
78 | }
79 |
80 | // this.getPaymentDetails();
81 |
82 | }).catch((error) => {
83 | console.log('error ', error);
84 | // this.getPaymentDetails();
85 | window.location.href = '/payments';
86 | })
87 | }
88 |
89 | updateCard = (paymentMethod) => {
90 | const url = '/api/payments/cards';
91 |
92 | fetch(url, {
93 | headers: {
94 | 'Accept': 'application/json',
95 | 'Content-Type': 'application/json'
96 | },
97 | credentials: 'same-origin',
98 | method: 'POST',
99 | body: JSON.stringify({
100 | paymentMethod
101 | })
102 | }).then((response) => {
103 | if (!response.ok) {
104 | return response.json().then(err => { throw err })
105 | }
106 | return response.json();
107 | }).then((results) => {
108 | console.log('results ', results);
109 | // this.getPaymentDetails();
110 | window.location.href = '/payments';
111 |
112 | }).catch((error) => {
113 | console.log('error ', error);
114 | // this.getPaymentDetails();
115 | window.location.href = '/payments';
116 | })
117 | }
118 |
119 | deleteSubscription = () => {
120 | const url = '/api/payments/subscriptions';
121 |
122 | fetch(url, {
123 | headers: {
124 | 'Accept': 'application/json',
125 | 'Content-Type': 'application/json'
126 | },
127 | credentials: 'same-origin',
128 | method: 'DELETE'
129 | }).then((response) => {
130 | if (!response.ok) {
131 | return response.json().then(err => { throw err })
132 | }
133 | return response.json();
134 | }).then((results) => {
135 | console.log('results ', results);
136 | // this.getPaymentDetails();
137 | window.location.href = '/payments';
138 |
139 | }).catch((error) => {
140 | console.log('error ', error);
141 | // this.getPaymentDetails();
142 | window.location.href = '/payments';
143 | })
144 | }
145 |
146 | getSubscription = (paymentMethod) => {
147 | const url = '/api/payments/subscriptions';
148 |
149 | fetch(url, {
150 | headers: {
151 | 'Accept': 'application/json',
152 | 'Content-Type': 'application/json'
153 | },
154 | credentials: 'same-origin',
155 | method: 'GET'
156 | }).then((response) => {
157 | if (!response.ok) {
158 | return response.json().then(err => { throw err })
159 | }
160 | return response.json();
161 | }).then((results) => {
162 | console.log('results ', results);
163 | this.setState({
164 | subscription: results.subscription
165 | })
166 |
167 | }).catch((error) => {
168 | console.log('error ', error);
169 | })
170 | }
171 |
172 | getCard = () => {
173 | const url = '/api/payments/cards';
174 |
175 | fetch(url, {
176 | headers: {
177 | 'Accept': 'application/json',
178 | 'Content-Type': 'application/json'
179 | },
180 | credentials: 'same-origin',
181 | method: 'GET'
182 | }).then((response) => {
183 | if (!response.ok) {
184 | return response.json().then(err => { throw err })
185 | }
186 | return response.json();
187 | }).then((results) => {
188 | console.log('results ', results);
189 | this.setState({
190 | card: results.card
191 | })
192 |
193 | }).catch((error) => {
194 | console.log('error ', error);
195 | })
196 | }
197 |
198 | render() {
199 | const customer = _get(this.props.user, 'stripeDetails.customer');
200 |
201 | return (
202 |
203 |
204 |
Payments
205 |
206 |
207 |
208 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | {
221 | customer &&
222 |
227 | }
228 |
229 |
237 |
238 |
239 | );
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/client/src/payments/StripeProviderComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StripeProvider, Elements } from 'react-stripe-elements';
3 | import CreditCardForm from './CreditCardForm';
4 |
5 | export default class StripeProviderComponent extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/payments/UpdateCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from 'react-bootstrap/Card';
3 | import StripeProviderComponent from './StripeProviderComponent';
4 |
5 | export default class UpdateCard extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 | Update Card
11 |
12 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { createLogger } from 'redux-logger';
4 | import rootReducer from './rootReducer';
5 |
6 | let store;
7 |
8 | const configureStore = (initialState) => {
9 | const middleware = [];
10 | // const enhancers = [];
11 |
12 | // THUNK
13 | middleware.push(thunk);
14 |
15 | // Logging Middleware
16 | const logger = createLogger({
17 | level: 'info',
18 | collapsed: true
19 | });
20 | middleware.push(logger);
21 |
22 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
23 |
24 | store = createStore(rootReducer, initialState, composeEnhancers(
25 | applyMiddleware(...middleware)
26 | ));
27 |
28 | }
29 |
30 |
31 | if (!store) {
32 | configureStore();
33 | }
34 |
35 | export default store;
36 |
--------------------------------------------------------------------------------
/client/src/store/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import auth from '../account/reducerAuth';
3 |
4 | const rootReducer = combineReducers({
5 | auth
6 | });
7 |
8 | export default rootReducer;
9 |
--------------------------------------------------------------------------------
/client2/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_cPAYuvk1DKJ0cqm0cm0Zblms
2 |
--------------------------------------------------------------------------------
/client2/.env-sample:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE
2 |
--------------------------------------------------------------------------------
/client2/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client2/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/client2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.3",
7 | "@testing-library/react": "^12.1.4",
8 | "@testing-library/user-event": "^13.5.0",
9 | "bootstrap": "^5.1.3",
10 | "js-cookie": "^3.0.1",
11 | "lodash": "^4.17.21",
12 | "query-string": "^7.1.1",
13 | "react": "^18.0.0",
14 | "react-bootstrap": "^2.2.2",
15 | "react-dom": "^18.0.0",
16 | "react-notifications": "^1.7.3",
17 | "react-redux": "^7.2.8",
18 | "react-router-dom": "^5.3.0",
19 | "react-scripts": "5.0.0",
20 | "react-stripe-elements": "^6.1.2",
21 | "redux": "^4.1.2",
22 | "redux-logger": "^3.0.6",
23 | "redux-thunk": "^2.4.1",
24 | "sass": "^1.49.10",
25 | "web-vitals": "^2.1.4"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "proxy": "http://localhost:3001",
52 | "engines": {
53 | "node": ">=14.0.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client2/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client2/public/favicon.ico
--------------------------------------------------------------------------------
/client2/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 | SaasiFy
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/client2/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client2/public/logo192.png
--------------------------------------------------------------------------------
/client2/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/client2/public/logo512.png
--------------------------------------------------------------------------------
/client2/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client2/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client2/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Route
4 | } from 'react-router-dom';
5 | import HomePage from './HomePage';
6 | import Register from './account/Register';
7 | import AccountActivation from './account/AccountActivation';
8 | import ResendActivationLink from './account/ResendActivationLink';
9 | import ResetPasswordLink from './account/ResetPasswordLink';
10 | import ResetPassword from './account/ResetPassword';
11 | import Login from './account/Login';
12 | import Logout from './account/Logout';
13 | import Header from './header/Header'
14 | import Payments from './payments/Payments';
15 | import { NotificationContainer } from 'react-notifications';
16 | import { connect } from 'react-redux';
17 | import { bindActionCreators } from 'redux';
18 | import { fetchLoggedInUser } from './account/actionsAuth';
19 | import 'bootstrap/dist/css/bootstrap.min.css';
20 | import 'react-notifications/dist/react-notifications.css';
21 | import './App.scss';
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return bindActionCreators({
25 | fetchLoggedInUser
26 | }, dispatch);
27 | };
28 |
29 | class App extends React.Component {
30 | state = {
31 | user: null
32 | }
33 |
34 | componentDidMount() {
35 | this.props.fetchLoggedInUser();
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
}
58 | />
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export default connect(null, mapDispatchToProps)(App);
66 |
--------------------------------------------------------------------------------
/client2/src/App.scss:
--------------------------------------------------------------------------------
1 | .app-container {
2 | padding-top: 16px;
3 | }
4 |
5 | body {
6 | background: #E3E8EE;
7 | font-family: 'Rubik', sans-serif;
8 | }
--------------------------------------------------------------------------------
/client2/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client2/src/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router';
3 | import ListGroup from 'react-bootstrap/ListGroup';
4 | import { Link } from 'react-router-dom';
5 | import { useSelector } from 'react-redux';
6 | import _get from 'lodash/get';
7 |
8 | function HomePage() {
9 | const auth = useSelector(state => state.auth);
10 | const email = _get(auth, 'user.email');
11 |
12 | return (
13 |
14 |
{email ? `Welcome ${email}` : 'Welcome Home'}
15 |
16 |
17 | Register
18 |
19 |
20 | Login
21 |
22 |
23 | Logout
24 |
25 |
26 | Activate
27 |
28 |
29 | Resend Activation Link
30 |
31 |
32 | Reset Password Link
33 |
34 |
35 | Reset Password
36 |
37 |
38 | Payments
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default withRouter(HomePage);
47 |
--------------------------------------------------------------------------------
/client2/src/account/AccountActivation.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import queryString from 'query-string';
3 | import Alert from 'react-bootstrap/Alert';
4 | import { Link } from 'react-router-dom';
5 |
6 | export default function AccountActivation(props) {
7 | const [busy, setBusy] = useState(false);
8 | const [apiState, setApiState] = useState({
9 | success: false,
10 | error: false,
11 | errors: []
12 | })
13 | console.log('props', props);
14 |
15 | useEffect(() => {
16 | // Get token from URL
17 | const queryStringParams = queryString.parse(props.location.search) || {};
18 | const token = queryStringParams.token;
19 |
20 | if (!token) {
21 | setBusy(false);
22 | setApiState({
23 | success: false,
24 | error: true,
25 | errors: []
26 | })
27 |
28 | return;
29 |
30 | }
31 |
32 | setBusy(true);
33 |
34 | const url = '/api/account-activate';
35 |
36 | fetch(url, {
37 | headers: {
38 | 'Accept': 'application/json',
39 | 'Content-Type': 'application/json'
40 | },
41 | credentials: 'same-origin',
42 | method: 'POST',
43 | body: JSON.stringify({
44 | activationToken: token
45 | })
46 | }).then((response) => {
47 | setBusy(false);
48 | if (!response.ok) {
49 | return response.json().then(err => { throw err })
50 | }
51 | return response.json();
52 | }).then((results) => {
53 | setApiState({
54 | success: true,
55 | error: false,
56 | errors: []
57 | })
58 | console.log('results ', results);
59 |
60 | }).catch((error) => {
61 | setApiState({
62 | success: false,
63 | error: true,
64 | errors: error.errors
65 | })
66 | console.log('error ', error);
67 | })
68 | }, []);
69 |
70 | const {
71 | success,
72 | error
73 | } = apiState;
74 |
75 | return (
76 |
77 | {
78 | busy ?
79 |
Activating Account ....
:
80 | null
81 | }
82 | {
83 | error &&
84 | (
85 |
86 |
87 | An error occurred. Perhaps you requested a new token?
88 |
89 |
90 | Reuqest a new Activation Token
91 |
92 |
93 |
94 | )
95 |
96 | }
97 |
98 | {
99 | success &&
100 | (
101 |
102 |
103 | Successfully activated your account. Please proceed to the Login page to Sign In
104 |
105 |
106 | Login
107 |
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
115 |
116 | )
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/client2/src/account/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import Cookies from 'js-cookie';
5 | import { Link } from 'react-router-dom';
6 | import { NotificationManager } from 'react-notifications';
7 | import Errors from '../common/Errors';
8 | import './account.scss';
9 |
10 | export default function Login(props) {
11 | const [email, setEmail] = useState('');
12 | const [password, setPassword] = useState('');
13 | const [apiState, setApiState] = useState({
14 | success: false,
15 | error: false,
16 | errors: []
17 | })
18 |
19 | const handleEmailChange = (event) => {
20 | setEmail(event.target.value);
21 | }
22 |
23 | const handlePasswordChange = (event) => {
24 | setPassword(event.target.value)
25 | }
26 |
27 | const handleSubmitClick = () => {
28 | setApiState({
29 | success: false,
30 | error: false,
31 | errors: null
32 | })
33 |
34 | // Submit the email and password to the server
35 | const url = '/api/login';
36 |
37 | fetch(url, {
38 | headers: {
39 | 'Accept': 'application/json',
40 | 'Content-Type': 'application/json'
41 | },
42 | credentials: 'same-origin',
43 | method: 'POST',
44 | body: JSON.stringify({
45 | email: email,
46 | password: password
47 | })
48 | }).then((response) => {
49 | if (!response.ok) {
50 | return response.json().then(err => { throw err })
51 | }
52 | return response.json();
53 | }).then((results) => {
54 | console.log('results ', results);
55 | const token = results.token;
56 |
57 | Cookies.set('token', token, {
58 | expires: 7
59 | })
60 | NotificationManager.success('You have successfully logged in', 'Login Success');
61 | setTimeout(() => {
62 | window.location.href = '/';
63 | }, 2000);
64 | }).catch((error) => {
65 | console.log('error ', error);
66 | setApiState({
67 | success: false,
68 | error: true,
69 | errors: error.errors
70 | });
71 | })
72 | }
73 |
74 | const {
75 | errors
76 | } = apiState;
77 |
78 | return (
79 |
80 |
81 |
Login
82 |
83 |
88 | Email address
89 |
95 |
96 | We'll never share your email with anyone else.
97 |
98 |
99 |
100 |
101 | Password
102 |
108 |
109 |
116 |
117 |
118 |
119 |
120 |
Don't have an account?
121 |
Register
122 |
123 |
124 |
Trouble Signing Up?
125 |
Reset Password
126 |
127 |
128 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/client2/src/account/Logout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Cookies from 'js-cookie';
3 |
4 | export default function Logout(props) {
5 | const [isLoggedOut, setIsLoggedOut] = useState(false);
6 |
7 | useEffect(() => {
8 | Cookies.remove('token');
9 | setIsLoggedOut(true);
10 | }, []);
11 |
12 | return (
13 |
14 |
15 | {
16 | isLoggedOut ?
17 |
You are logged out
:
18 |
You are logged in
19 | }
20 |
21 |
22 | );
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/client2/src/account/Register.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function Register(props) {
10 | const [email, setEmail] = useState('');
11 | const [password, setPassword] = useState('');
12 | const [apiState, setApiState] = useState({
13 | success: false,
14 | error: false,
15 | errors: []
16 | })
17 |
18 | const handleEmailChange = (event) => {
19 | setEmail(event.target.value);
20 | }
21 |
22 | const handlePasswordChange = (event) => {
23 | setPassword(event.target.value)
24 | }
25 |
26 | const handleSubmitClick = () => {
27 | setApiState({
28 | success: false,
29 | error: false,
30 | errors: null
31 | })
32 |
33 | // Submit the email and password to the server
34 | const url = '/api/register';
35 |
36 | fetch(url, {
37 | headers: {
38 | 'Accept': 'application/json',
39 | 'Content-Type': 'application/json'
40 | },
41 | credentials: 'same-origin',
42 | method: 'POST',
43 | body: JSON.stringify({
44 | email,
45 | password
46 | })
47 | }).then((response) => {
48 | if (!response.ok) {
49 | return response.json().then(err => { throw err })
50 | }
51 | return response.json();
52 | }).then((results) => {
53 | console.log('results ', results);
54 | setApiState({
55 | success: true,
56 | error: false,
57 | errors: null
58 | })
59 | }).catch((error) => {
60 | console.log('error ', error);
61 | setApiState({
62 | success: false,
63 | error: true,
64 | errors: error.errors
65 | })
66 | })
67 | }
68 |
69 | const {
70 | success,
71 | errors
72 | } = apiState;
73 |
74 | return (
75 |
76 |
77 |
Register
78 |
79 |
92 | Email address
93 |
99 |
100 | We'll never share your email with anyone else.
101 |
102 |
103 |
104 |
105 | Password
106 |
112 |
113 |
120 |
121 |
122 |
123 |
Already have an account?
124 |
Login
125 |
126 |
127 |
128 | )
129 | }
130 |
--------------------------------------------------------------------------------
/client2/src/account/ResendActivationLink.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function ResendActivationLink(props) {
10 | const [email, setEmail] = useState('');
11 | const [apiState, setApiState] = useState({
12 | success: false,
13 | error: false,
14 | errors: []
15 | })
16 |
17 | const handleEmailChange = (event) => {
18 | setEmail(event.target.value);
19 | }
20 |
21 | const handleSubmitClick = () => {
22 | setApiState({
23 | success: false,
24 | error: false,
25 | errors: null
26 | })
27 |
28 | // Submit the email and password to the server
29 | const url = '/api/resend-activation-link';
30 |
31 | fetch(url, {
32 | headers: {
33 | 'Accept': 'application/json',
34 | 'Content-Type': 'application/json'
35 | },
36 | credentials: 'same-origin',
37 | method: 'POST',
38 | body: JSON.stringify({
39 | email
40 | })
41 | }).then((response) => {
42 | if (!response.ok) {
43 | return response.json().then(err => { throw err })
44 | }
45 | return response.json();
46 | }).then((results) => {
47 | console.log('results ', results);
48 | setApiState({
49 | success: true,
50 | error: false,
51 | errors: null
52 | })
53 |
54 | }).catch((error) => {
55 | console.log('error ', error);
56 | setApiState({
57 | success: false,
58 | error: true,
59 | errors: error.errors
60 | })
61 |
62 | })
63 | }
64 |
65 | const {
66 | success,
67 | errors
68 | } = apiState;
69 |
70 | return (
71 |
72 |
73 |
Resend Activation Link
74 |
75 |
87 | Email address
88 |
94 |
95 | This account should already exist in our database.
96 |
97 |
98 |
99 |
106 |
107 |
108 |
109 |
110 |
111 |
Already have an account?
112 |
Login
113 |
114 |
115 |
Don't have an account?
116 |
Register
117 |
118 |
119 |
120 |
121 |
122 | )
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/client2/src/account/ResetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import queryString from 'query-string';
3 | import Form from 'react-bootstrap/Form';
4 | import Button from 'react-bootstrap/Button';
5 | import { Link } from 'react-router-dom';
6 | import Alert from 'react-bootstrap/Alert';
7 | import Errors from '../common/Errors';
8 | import './account.scss';
9 |
10 | export default function ResetPassword(props) {
11 | const [password, setPassword] = useState('');
12 | const [resetPasswordToken, setResetPasswordToken] = useState('');
13 | const [apiState, setApiState] = useState({
14 | success: false,
15 | error: false,
16 | errors: []
17 | })
18 |
19 | const handlePasswordChange = (event) => {
20 | setPassword(event.target.value)
21 | }
22 |
23 | useEffect(() => {
24 | // Get token from URL
25 | const queryStringParams = queryString.parse(props.location.search) || {};
26 | const token = queryStringParams.token;
27 |
28 | if (!token) {
29 | setApiState({
30 | success: false,
31 | error: true,
32 | errors: [{
33 | message: 'You need to generate a Reset Password Token before resetting your password'
34 | }]
35 | })
36 |
37 |
38 | }
39 |
40 | setResetPasswordToken(token);
41 | }, []);
42 |
43 | const handleSubmitClick = () => {
44 | setApiState({
45 | success: false,
46 | error: false,
47 | errors: null
48 | })
49 |
50 | // Submit the email and password to the server
51 | const url = '/api/reset-password';
52 |
53 | fetch(url, {
54 | headers: {
55 | 'Accept': 'application/json',
56 | 'Content-Type': 'application/json'
57 | },
58 | credentials: 'same-origin',
59 | method: 'POST',
60 | body: JSON.stringify({
61 | resetPasswordToken: resetPasswordToken,
62 | password: password
63 | })
64 | }).then((response) => {
65 | if (!response.ok) {
66 | return response.json().then(err => { throw err })
67 | }
68 | return response.json();
69 | }).then((results) => {
70 | console.log('results ', results);
71 | setApiState({
72 | success: true,
73 | error: false,
74 | errors: null
75 | })
76 |
77 | }).catch((error) => {
78 | console.log('error ', error);
79 | setApiState({
80 | success: false,
81 | error: true,
82 | errors: error.errors
83 | })
84 | })
85 |
86 | }
87 |
88 | const {
89 | success,
90 | errors
91 | } = apiState;
92 |
93 | return (
94 |
95 |
96 |
Reset Password
97 |
98 |
110 | Password
111 |
117 |
118 | Please enter your new password
119 |
120 |
121 |
122 |
129 |
130 |
131 |
132 |
133 |
134 |
Try Login again?
135 |
Login
136 |
137 |
138 |
139 |
140 |
141 | );
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/client2/src/account/ResetPasswordLink.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import { Link } from 'react-router-dom';
5 | import Alert from 'react-bootstrap/Alert';
6 | import Errors from '../common/Errors';
7 | import './account.scss';
8 |
9 | export default function ResetPasswordLink(props) {
10 | const [email, setEmail] = useState('');
11 | const [apiState, setApiState] = useState({
12 | success: false,
13 | error: false,
14 | errors: []
15 | })
16 |
17 | const handleEmailChange = (event) => {
18 | setEmail(event.target.value);
19 | }
20 |
21 | const handleSubmitClick = () => {
22 | setApiState({
23 | success: false,
24 | error: false,
25 | errors: null
26 | })
27 |
28 | // Submit the email and password to the server
29 | const url = '/api/reset-password-link';
30 |
31 | fetch(url, {
32 | headers: {
33 | 'Accept': 'application/json',
34 | 'Content-Type': 'application/json'
35 | },
36 | credentials: 'same-origin',
37 | method: 'POST',
38 | body: JSON.stringify({
39 | email
40 | })
41 | }).then((response) => {
42 | if (!response.ok) {
43 | return response.json().then(err => { throw err })
44 | }
45 | return response.json();
46 | }).then((results) => {
47 | console.log('results ', results);
48 | setApiState({
49 | success: true,
50 | error: false,
51 | errors: null
52 | })
53 |
54 | }).catch((error) => {
55 | console.log('error ', error);
56 | setApiState({
57 | success: false,
58 | error: true,
59 | errors: error.errors
60 | })
61 | })
62 | }
63 | const {
64 | success,
65 | errors
66 | } = apiState;
67 |
68 | return (
69 |
70 |
71 |
Reset Password Link
72 |
73 |
85 | Email address
86 |
92 |
93 | This account should already exist in our database.
94 |
95 |
96 |
97 |
104 |
105 |
106 |
107 |
108 |
Try Login again?
109 |
Login
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 |
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/client2/src/account/account.scss:
--------------------------------------------------------------------------------
1 | .form-container {
2 | max-width: 340px;
3 | margin: 0 auto;
4 | background: white;
5 | padding: 16px;
6 | }
7 |
8 | .actions-container {
9 | margin-top: 24px;
10 | }
--------------------------------------------------------------------------------
/client2/src/account/actionsAuth.js:
--------------------------------------------------------------------------------
1 | export const AUTH_BUSY = 'AUTH_BUSY';
2 | export const AUTH_RETRIEVEUSER_SUCCESS = 'AUTH_RETRIEVEUSER_SUCCESS';
3 | export const AUTH_RETRIEVEUSER_ERROR = 'AUTH_RETRIEVEUSER_ERROR';
4 |
5 | export function fetchLoggedInUser() {
6 | return function(dispatch, getState) {
7 | dispatch({
8 | type: AUTH_BUSY
9 | });
10 | const url = '/api/logged-in-user';
11 |
12 | fetch(url, {
13 | headers: {
14 | 'Accept': 'application/json',
15 | 'Content-Type': 'application/json'
16 | },
17 | credentials: 'same-origin',
18 | method: 'GET'
19 | }).then((response) => {
20 | if (!response.ok) {
21 | return response.json().then(err => { throw err })
22 | }
23 | return response.json();
24 | }).then((results) => {
25 | console.log('results ', results);
26 | const user = results.user;
27 |
28 | dispatch({
29 | type: AUTH_RETRIEVEUSER_SUCCESS,
30 | payload: user
31 | });
32 | }).catch((error) => {
33 | console.log('error ', error);
34 | dispatch({
35 | type: AUTH_RETRIEVEUSER_ERROR,
36 | payload: null
37 | });
38 | })
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/client2/src/account/reducerAuth.js:
--------------------------------------------------------------------------------
1 | import _isEmpty from 'lodash/isEmpty';
2 |
3 | import {
4 | AUTH_BUSY,
5 | AUTH_RETRIEVEUSER_SUCCESS,
6 | AUTH_RETRIEVEUSER_ERROR
7 | } from './actionsAuth';
8 |
9 | export default function auth(state = {}, action) {
10 | switch(action.type) {
11 | case AUTH_BUSY: {
12 | return {
13 | ...state,
14 | busy: true
15 | }
16 | }
17 |
18 | case AUTH_RETRIEVEUSER_ERROR: {
19 | return {
20 | ...state,
21 | busy: false,
22 | error: null,
23 | authenticated: false,
24 | user: null
25 | }
26 | }
27 | case AUTH_RETRIEVEUSER_SUCCESS: {
28 | const { payload = {} } = action;
29 | return {
30 | ...state,
31 | busy: false,
32 | error: null,
33 | authenticated: !_isEmpty(payload),
34 | user: payload
35 | }
36 | }
37 | default: return state;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client2/src/common/Errors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Alert from 'react-bootstrap/Alert';
3 | import _map from 'lodash/map';
4 |
5 | export default class Errors extends React.Component {
6 | render() {
7 | const {
8 | errors
9 | } = this.props;
10 |
11 | const errorMessages = _map(errors, 'message');
12 |
13 | return (
14 |
15 | {
16 | _map(errorMessages, (error, index) => {
17 | return (
18 |
19 | {error}
20 |
21 | )
22 | })
23 | }
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client2/src/header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Navbar from 'react-bootstrap/Navbar';
3 | import Nav from 'react-bootstrap/Nav';
4 |
5 | export class Header extends Component {
6 | render() {
7 | return (
8 |
9 | <>
10 |
11 | SAASIFY
12 |
17 |
18 | >
19 |
20 | );
21 | }
22 | }
23 |
24 | export default Header;
25 |
--------------------------------------------------------------------------------
/client2/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/client2/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { Provider } from 'react-redux';
4 | import store from './store/configureStore';
5 | import './index.css';
6 | import App from './App';
7 | import * as serviceWorker from './serviceWorker';
8 | import {
9 | BrowserRouter as Router
10 | } from 'react-router-dom';
11 |
12 | const root = ReactDOM.createRoot(document.getElementById('root'));
13 |
14 | root.render(
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
22 | // If you want your app to work offline and load faster, you can change
23 | // unregister() to register() below. Note this comes with some pitfalls.
24 | // Learn more about service workers: https://bit.ly/CRA-PWA
25 | serviceWorker.unregister();
26 |
--------------------------------------------------------------------------------
/client2/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client2/src/payments/CreateSubscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from 'react-bootstrap/Form';
3 | import Button from 'react-bootstrap/Button';
4 | import StripeProviderComponent from './StripeProviderComponent';
5 | import Card from 'react-bootstrap/Card';
6 | import _get from 'lodash/get';
7 |
8 | export default class CreateSubscription extends React.Component {
9 | handlePlanChange = (event) => {
10 | this.props.setPlan(event.target.value)
11 | }
12 |
13 | createSubscription = () => {
14 | this.props.onPaymentMethodCreated();
15 | }
16 |
17 | render() {
18 | const {
19 | user
20 | } = this.props;
21 |
22 | const hasSubscription = _get(user, 'stripeDetails.subscription.status', '') === 'active';
23 |
24 | return (
25 |
26 |
27 | {
28 | hasSubscription ?
29 | Update your subscription
:
30 | Subscribe to a plan
31 | }
32 |
33 |
35 | Select your plan
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {
49 | !this.props.card &&
50 |
55 | }
56 |
57 | {
58 | this.props.card &&
59 |
66 | }
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/client2/src/payments/CreditCardForm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Use the CSS tab above to style your Element's container.
3 | */
4 | import React from 'react';
5 | import { CardElement, injectStripe } from 'react-stripe-elements';
6 | import Button from 'react-bootstrap/Button';
7 |
8 | const style = {
9 | base: {
10 | color: "#32325d",
11 | fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
12 | fontSmoothing: "antialiased",
13 | fontSize: "16px",
14 | "::placeholder": {
15 | color: "#aab7c4"
16 | }
17 | },
18 | invalid: {
19 | color: "#fa755a",
20 | iconColor: "#fa755a"
21 | }
22 | };
23 |
24 | class CardSection extends React.Component {
25 | handleSubmitClick = async (event) => {
26 |
27 | const cardElement = this.props.elements.getElement('card');
28 |
29 | const { paymentMethod, error } = await this.props.stripe.createPaymentMethod({
30 | type: 'card',
31 | card: cardElement,
32 | billing_details: {
33 | email: this.props.user.email,
34 | },
35 | });
36 |
37 | console.log('paymentMethod', paymentMethod);
38 | console.log('error', error);
39 |
40 | if (!error) {
41 | this.props.onPaymentMethodCreated(paymentMethod);
42 | }
43 | }
44 |
45 | render() {
46 | return (
47 |
63 | );
64 | }
65 | };
66 |
67 | export default injectStripe(CardSection);
68 |
--------------------------------------------------------------------------------
/client2/src/payments/CurrentCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _get from 'lodash/get';
3 | import Card from 'react-bootstrap/Card';
4 |
5 | export default class CurrentCard extends React.Component {
6 | render() {
7 | const {
8 | card
9 | } = this.props;
10 |
11 | const brand = _get(card, 'brand');
12 | const exp_month = _get(card, 'exp_month');
13 | const exp_year = _get(card, 'exp_year');
14 | const last4 = _get(card, 'last4');
15 |
16 | if (!card) {
17 | return (
18 |
19 |
20 | Card details
21 | You do not have any saved cards.
22 |
23 |
24 | )
25 | }
26 |
27 | return (
28 |
29 |
30 | Card details
31 |
32 |
Brand
33 |
{brand}
34 |
Expiry Month
35 |
{exp_month}
36 |
Expiry Year
37 |
{exp_year}
38 |
Last 4
39 |
{last4}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client2/src/payments/CurrentSubscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from 'react-bootstrap/Card';
3 | import Button from 'react-bootstrap/Button';
4 |
5 | export default class CurrentSubscription extends React.Component {
6 | render() {
7 | const {
8 | subscription
9 | } = this.props;
10 | if (!subscription) {
11 | return (
12 |
13 |
14 | Subscription details
15 | You do not have any subscriptions yet.
16 |
17 |
18 | )
19 | }
20 |
21 | const nickname = subscription.plan.nickname;
22 | const amount = subscription.plan.amount;
23 | const interval = subscription.plan.interval;
24 | const status = subscription.status;
25 |
26 |
27 | return (
28 |
29 |
30 | Subscription details
31 |
32 |
Status
33 |
{status}
34 |
You are subscribed to
35 |
{nickname}
36 |
We will charge you
37 |
{amount}
38 |
Interval
39 |
{interval}
40 |
41 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/client2/src/payments/Payments.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CurrentSubscription from './CurrentSubscription';
3 | import CurrentCard from './CurrentCard';
4 | import CreateSubscription from './CreateSubscription';
5 | import UpdateCard from './UpdateCard';
6 | import _get from 'lodash/get';
7 |
8 | const stripe = Stripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); // eslint-disable-line
9 |
10 | export default class Payments extends React.Component {
11 | state = {
12 | subscription: null,
13 | card: null,
14 | plan: ''
15 | }
16 | componentDidMount() {
17 | this.getPaymentDetails();
18 | }
19 |
20 | getPaymentDetails = () => {
21 | this.getSubscription();
22 | this.getCard();
23 | }
24 |
25 | setPlan = (planId) => {
26 | this.setState({
27 | plan: planId
28 | })
29 | }
30 |
31 | createSubscription = (paymentMethod) => {
32 | if (!this.state.plan) {
33 | // Show error to user
34 | return;
35 | }
36 |
37 | const planId = this.state.plan;
38 |
39 | const url = '/api/payments/subscriptions';
40 |
41 | fetch(url, {
42 | headers: {
43 | 'Accept': 'application/json',
44 | 'Content-Type': 'application/json'
45 | },
46 | credentials: 'same-origin',
47 | method: 'POST',
48 | body: JSON.stringify({
49 | planId: planId,
50 | paymentMethod
51 | })
52 | }).then((response) => {
53 | if (!response.ok) {
54 | return response.json().then(err => { throw err })
55 | }
56 | return response.json();
57 | }).then((results) => {
58 | console.log('results ', results);
59 | const subscription = _get(results, 'subscription');
60 | const paymentIntentStatus = _get(subscription, 'latest_invoice.payment_intent.status', '');
61 | const client_secret = _get(subscription, 'latest_invoice.payment_intent.client_secret', '');
62 | if (paymentIntentStatus === 'requires_action' || paymentIntentStatus === 'requires_payment_method') {
63 | stripe.confirmCardPayment(client_secret).then(function(result) {
64 | console.log('confirmCardPayment result', result);
65 | if (result.error) {
66 | // Display error message in your UI.
67 | // The card was declined (i.e. insufficient funds, card has expired, etc)
68 | alert(result.error);
69 |
70 | } else {
71 | // Show a success message to your customer
72 | alert('You have successfully confirmed the payment');
73 | window.location.href = '/payments';
74 | }
75 |
76 |
77 | });
78 | }
79 |
80 | // this.getPaymentDetails();
81 |
82 | }).catch((error) => {
83 | console.log('error ', error);
84 | // this.getPaymentDetails();
85 | window.location.href = '/payments';
86 | })
87 | }
88 |
89 | updateCard = (paymentMethod) => {
90 | const url = '/api/payments/cards';
91 |
92 | fetch(url, {
93 | headers: {
94 | 'Accept': 'application/json',
95 | 'Content-Type': 'application/json'
96 | },
97 | credentials: 'same-origin',
98 | method: 'POST',
99 | body: JSON.stringify({
100 | paymentMethod
101 | })
102 | }).then((response) => {
103 | if (!response.ok) {
104 | return response.json().then(err => { throw err })
105 | }
106 | return response.json();
107 | }).then((results) => {
108 | console.log('results ', results);
109 | // this.getPaymentDetails();
110 | window.location.href = '/payments';
111 |
112 | }).catch((error) => {
113 | console.log('error ', error);
114 | // this.getPaymentDetails();
115 | window.location.href = '/payments';
116 | })
117 | }
118 |
119 | deleteSubscription = () => {
120 | const url = '/api/payments/subscriptions';
121 |
122 | fetch(url, {
123 | headers: {
124 | 'Accept': 'application/json',
125 | 'Content-Type': 'application/json'
126 | },
127 | credentials: 'same-origin',
128 | method: 'DELETE'
129 | }).then((response) => {
130 | if (!response.ok) {
131 | return response.json().then(err => { throw err })
132 | }
133 | return response.json();
134 | }).then((results) => {
135 | console.log('results ', results);
136 | // this.getPaymentDetails();
137 | window.location.href = '/payments';
138 |
139 | }).catch((error) => {
140 | console.log('error ', error);
141 | // this.getPaymentDetails();
142 | window.location.href = '/payments';
143 | })
144 | }
145 |
146 | getSubscription = (paymentMethod) => {
147 | const url = '/api/payments/subscriptions';
148 |
149 | fetch(url, {
150 | headers: {
151 | 'Accept': 'application/json',
152 | 'Content-Type': 'application/json'
153 | },
154 | credentials: 'same-origin',
155 | method: 'GET'
156 | }).then((response) => {
157 | if (!response.ok) {
158 | return response.json().then(err => { throw err })
159 | }
160 | return response.json();
161 | }).then((results) => {
162 | console.log('results ', results);
163 | this.setState({
164 | subscription: results.subscription
165 | })
166 |
167 | }).catch((error) => {
168 | console.log('error ', error);
169 | })
170 | }
171 |
172 | getCard = () => {
173 | const url = '/api/payments/cards';
174 |
175 | fetch(url, {
176 | headers: {
177 | 'Accept': 'application/json',
178 | 'Content-Type': 'application/json'
179 | },
180 | credentials: 'same-origin',
181 | method: 'GET'
182 | }).then((response) => {
183 | if (!response.ok) {
184 | return response.json().then(err => { throw err })
185 | }
186 | return response.json();
187 | }).then((results) => {
188 | console.log('results ', results);
189 | this.setState({
190 | card: results.card
191 | })
192 |
193 | }).catch((error) => {
194 | console.log('error ', error);
195 | })
196 | }
197 |
198 | render() {
199 | const customer = _get(this.props.user, 'stripeDetails.customer');
200 |
201 | return (
202 |
203 |
204 |
Payments
205 |
206 |
207 |
208 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | {
221 | customer &&
222 |
227 | }
228 |
229 |
237 |
238 |
239 | );
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/client2/src/payments/StripeProviderComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StripeProvider, Elements } from 'react-stripe-elements';
3 | import CreditCardForm from './CreditCardForm';
4 |
5 | export default class StripeProviderComponent extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client2/src/payments/UpdateCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from 'react-bootstrap/Card';
3 | import StripeProviderComponent from './StripeProviderComponent';
4 |
5 | export default class UpdateCard extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 | Update Card
11 |
12 |
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client2/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client2/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { createLogger } from 'redux-logger';
4 | import rootReducer from './rootReducer';
5 |
6 | let store;
7 |
8 | const configureStore = (initialState) => {
9 | const middleware = [];
10 | // const enhancers = [];
11 |
12 | // THUNK
13 | middleware.push(thunk);
14 |
15 | // Logging Middleware
16 | const logger = createLogger({
17 | level: 'info',
18 | collapsed: true
19 | });
20 | middleware.push(logger);
21 |
22 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
23 |
24 | store = createStore(rootReducer, initialState, composeEnhancers(
25 | applyMiddleware(...middleware)
26 | ));
27 |
28 | }
29 |
30 |
31 | if (!store) {
32 | configureStore();
33 | }
34 |
35 | export default store;
36 |
--------------------------------------------------------------------------------
/client2/src/store/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import auth from '../account/reducerAuth';
3 |
4 | const rootReducer = combineReducers({
5 | auth
6 | });
7 |
8 | export default rootReducer;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "saas",
3 | "version": "1.0.0",
4 | "description": "SAASiFy is a sample SAAS app built with NodeJS and ReactJS. It is primarily built for the Udemy course [SaasiFy - Build a complete SAAS App this weekend](https://www.udemy.com/course/2786970).",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cd server && npm start",
8 | "heroku-postbuild": "cd client/ && npm install && npm run build && cd ../server && npm install"
9 | },
10 | "cacheDirectories": [
11 | "node_modules",
12 | "client/node_modules",
13 | "server/node_modules"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/saas-developer/saas-starter.git"
18 | },
19 | "author": "",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/saas-developer/saas-starter/issues"
23 | },
24 | "homepage": "https://github.com/saas-developer/saas-starter#readme",
25 | "engines" : {
26 | "node" : "12.16.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/.env-sample:
--------------------------------------------------------------------------------
1 | MONGODB_URI=mongodb://0.0.0.0/saas
2 | JWT_SECRET=very-long-secret-abcdef
3 | APP_PORT=3001
4 | APP_EMAIL=youremail@yourdomain.com
5 | MAILGUN_DOMAIN=sandbox-abcd.mailgun.org
6 | MAILGUN_API_KEY=key-abcd
7 | BASE_URL=http://localhost:3000
8 | STRIPE_PUBLISHABLE_KEY=pk_test_abcd
9 | STRIPE_SECRET_KEY=sk_test_abcd
10 | STRIPE_WEBHOOK_SIGNING_SECRET=whsec_abcd
11 |
--------------------------------------------------------------------------------
/server/controllers/authController.js:
--------------------------------------------------------------------------------
1 | const User = require('../models/User');
2 | const passport = require('passport');
3 | const jwt = require('jsonwebtoken');
4 | const uuidv1 = require('uuid/v1');
5 | const {
6 | sendAccountActivationEmail,
7 | resendActivationLink,
8 | sendResetPasswordLinkEmail
9 | } = require('../lib/EmailManager');
10 |
11 | exports.getLoggedInUser = async (req, res, next) => {
12 | const user = User.toClientObject(req.user);
13 |
14 | res.send({
15 | user
16 | })
17 | }
18 |
19 | exports.register = async (req, res, next) => {
20 | const {
21 | email,
22 | password
23 | } = req.body;
24 |
25 | // Validate the input fields
26 | const validationErrors = [];
27 |
28 | if (!email) {
29 | validationErrors.push({
30 | code: 'VALIDATION_ERROR',
31 | field: 'email',
32 | message: ' You must provide an email address'
33 | });
34 | }
35 |
36 | const isEmailValid = email && validateEmail(email);
37 | if (email && !isEmailValid) {
38 | validationErrors.push({
39 | code: 'VALIDATION_ERROR',
40 | field: 'email',
41 | message: 'Email is not valid'
42 | });
43 | }
44 |
45 |
46 | if (!password) {
47 | validationErrors.push({
48 | code: 'VALIDATION_ERROR',
49 | field: 'password',
50 | message: ' You must provide a password'
51 | });
52 | }
53 |
54 | if (validationErrors.length) {
55 | const errorObject = {
56 | error: true,
57 | errors: validationErrors
58 | };
59 |
60 | res.status(422).send(errorObject);
61 |
62 | return;
63 | }
64 |
65 | // Save this info to DB
66 |
67 | try {
68 | const existingUser = await User.findOne({ email });
69 | if (existingUser) {
70 | const errorObject = {
71 | error: true,
72 | errors: [{
73 | code: 'VALIDATION_ERROR',
74 | field: 'Email',
75 | message: 'Email already exists'
76 | }]
77 | };
78 |
79 | res.status(422).send(errorObject);
80 |
81 | return;
82 | }
83 |
84 |
85 | let user = new User({
86 | email,
87 | password,
88 | activated: false,
89 | activationToken: uuidv1(),
90 | activationTokenSentAt: Date.now()
91 | });
92 |
93 | const savedUser = await user.save();
94 |
95 | console.log('savedUser ', savedUser);
96 |
97 | // Here -> We will send the email
98 | sendAccountActivationEmail(savedUser);
99 |
100 | res.status(200).send({
101 | user: User.toClientObject(savedUser)
102 | })
103 |
104 | } catch (e) {
105 | console.log('e ', e);
106 | }
107 |
108 |
109 |
110 | }
111 |
112 | exports.login = async (req, res, next) => {
113 | const {
114 | email,
115 | password
116 | } = req.body;
117 | // Validate the input fields
118 | const validationErrors = [];
119 |
120 | if (!email) {
121 | validationErrors.push({
122 | code: 'VALIDATION_ERROR',
123 | field: 'email',
124 | message: ' You must provide an email address'
125 | });
126 | }
127 |
128 | const isEmailValid = email && validateEmail(email);
129 | if (email && !isEmailValid) {
130 | validationErrors.push({
131 | code: 'VALIDATION_ERROR',
132 | field: 'email',
133 | message: 'Email is not valid'
134 | });
135 | }
136 |
137 |
138 | if (!password) {
139 | validationErrors.push({
140 | code: 'VALIDATION_ERROR',
141 | field: 'password',
142 | message: ' You must provide a password'
143 | });
144 | }
145 |
146 | if (validationErrors.length) {
147 | const errorObject = {
148 | error: true,
149 | errors: validationErrors
150 | };
151 |
152 | res.status(422).send(errorObject);
153 |
154 | return;
155 | }
156 |
157 | passport.authenticate('local', (err, user, info) => {
158 | if (err) {
159 | return next(err);
160 | }
161 |
162 | if (!user) {
163 | res.status(401).send(info);
164 | return;
165 | }
166 | const errorObject = {
167 | error: true,
168 | errors: [{
169 | code: 'GLOBAL_ERROR',
170 | message: 'Account is not activated.'
171 | }]
172 | };
173 |
174 | if (!user.activated) {
175 | res.status(403).send(errorObject)
176 | return;
177 | }
178 |
179 | const userObject = user.toObject();
180 | const tokenObject = {
181 | _id: userObject._id
182 | }
183 |
184 | const jwtToken = jwt.sign(tokenObject, process.env.JWT_SECRET, {
185 | expiresIn: 86400 // seconds in a day
186 | })
187 |
188 | res.status(200).send({
189 | token: jwtToken,
190 | user: User.toClientObject(user)
191 | });
192 | return;
193 |
194 |
195 |
196 | })(req, res, next);
197 |
198 | }
199 |
200 | exports.accountActivate = async (req, res, next) => {
201 | const {
202 | activationToken
203 | } = req.body;
204 |
205 | if (!activationToken) {
206 | const errorObject = {
207 | error: true,
208 | errors: [{
209 | code: 'VALIDATION_ERROR',
210 | message: 'Invalid Activation Token. Perhaps you requested a new token?'
211 | }]
212 | };
213 |
214 | res.status(422).send(errorObject);
215 |
216 | return;
217 | }
218 |
219 | try {
220 | const user = await User.findOne({
221 | activationToken
222 | });
223 |
224 | if (!user) {
225 | const errorObject = {
226 | error: true,
227 | errors: [{
228 | code: 'VALIDATION_ERROR',
229 | message: 'Invalid Activation Token. Perhaps you requested a new token?'
230 | }]
231 | };
232 |
233 | res.status(422).send(errorObject);
234 |
235 | return;
236 | }
237 |
238 | // We found a user
239 | user.activated = true;
240 | user.activationToken = undefined;
241 | user.activatedAt = Date.now();
242 |
243 | const savedUser = await user.save();
244 |
245 | return res.send({
246 | message: 'Your account has been activated. Please proceed to the Login page to Sign In'
247 | });
248 |
249 |
250 | } catch (e) {
251 | console.log('e ', e);
252 | res.status(500).send({
253 | error: true
254 | })
255 | }
256 |
257 | }
258 |
259 | exports.resendActivationLink = async (req, res, next) => {
260 | const {
261 | email
262 | } = req.body;
263 |
264 | if (!email) {
265 | const errorObject = {
266 | error: true,
267 | errors: [{
268 | code: 'VALIDATION_ERROR',
269 | message: 'Please specify the email account that needs activation'
270 | }]
271 | };
272 |
273 | res.status(422).send(errorObject);
274 |
275 | return;
276 | }
277 |
278 | try {
279 | const user = await User.findOne({
280 | email
281 | });
282 |
283 | if (user && !user.activated) {
284 | user.activationToken = uuidv1();
285 | user.activationTokenSentAt = Date.now();
286 |
287 | await user.save();
288 |
289 | // Send activation email
290 | //...
291 | await resendActivationLink(user);
292 | }
293 |
294 |
295 | return res.send({
296 | message: 'Activation Link has been sent'
297 | })
298 |
299 | } catch (e) {
300 | console.log('e ', e);
301 | res.status(500).send({
302 | error: true
303 | })
304 | }
305 | }
306 |
307 | exports.resetPasswordLink = async ( req, res, next) => {
308 | const {
309 | email
310 | } = req.body;
311 |
312 | if (!email) {
313 | const errorObject = {
314 | error: true,
315 | errors: [{
316 | code: 'VALIDATION_ERROR',
317 | message: 'Please specify the email account that needs password reset'
318 | }]
319 | };
320 |
321 | res.status(422).send(errorObject);
322 |
323 | return;
324 | }
325 |
326 | try {
327 | const user = await User.findOne({
328 | email
329 | });
330 |
331 | if (user) {
332 | user.resetPasswordToken = uuidv1();
333 | user.resetPasswordTokenSentAt = Date.now();
334 |
335 | const savedUser = user.save();
336 |
337 | // Send email now ...
338 | sendResetPasswordLinkEmail(user);
339 |
340 | }
341 |
342 | return res.send({
343 | message: 'Reset Password Link has been sent'
344 | })
345 |
346 | } catch (e) {
347 | console.log('e ', e);
348 | res.status(500).send({
349 | error: true
350 | })
351 | }
352 |
353 | }
354 |
355 | exports.resetPassword = async (req, res, next) => {
356 | const {
357 | resetPasswordToken,
358 | password
359 | } = req.body;
360 |
361 | const validationErrors = [];
362 |
363 | if (!password || !resetPasswordToken) {
364 | validationErrors.push({
365 | code: 'VALIDATION_ERROR',
366 | field: '',
367 | message: 'Sorry, we could not update your password'
368 | })
369 | }
370 |
371 | if (validationErrors.length) {
372 | const errorObject = {
373 | error: true,
374 | errors: validationErrors
375 | };
376 |
377 | res.status(422).send(errorObject);
378 | return res.status(422).send(errorObject);
379 | }
380 |
381 | try {
382 | const user = await User.findOne({
383 | resetPasswordToken
384 | });
385 |
386 | if (!user) {
387 | const errorObject = {
388 | error: true,
389 | errors: [{
390 | code: 'GLOBAL_ERROR',
391 | message: 'Sorry, we could not update your password'
392 | }]
393 | };
394 |
395 | return res.status(422).send(errorObject);
396 | }
397 |
398 | if (user) {
399 | user.password = password;
400 | user.resetPasswordToken = undefined;
401 |
402 | const savedUser = await user.save();
403 | }
404 |
405 | res.status(200).send({
406 | message: 'Your password has been successfully updated. Please go to the Login Page to Sign In again'
407 | });
408 |
409 |
410 | } catch (e) {
411 | console.log('e ', e);
412 | res.status(500).send({
413 | error: true
414 | })
415 | }
416 |
417 |
418 |
419 | }
420 |
421 | exports.testAuth = async (req, res, next) => {
422 | console.log('req.user ', req.user);
423 |
424 | res.send({
425 | isLoggedIn: req.user ? true : false
426 | })
427 | }
428 |
429 |
430 | function validateEmail(email) {
431 | var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
432 | return re.test(String(email).toLowerCase());
433 | }
434 |
--------------------------------------------------------------------------------
/server/controllers/paymentsController.js:
--------------------------------------------------------------------------------
1 | const {
2 | createCustomer,
3 | createSubscription,
4 | updateSubscription,
5 | retrievePaymentMethod,
6 | deleteSubscription,
7 | attachPaymentMethodToCustomer,
8 | updateCustomer
9 | } = require('../lib/StripeManager');
10 | const User = require('../models/User');
11 | const _ = require('lodash');
12 | const stripe = require('stripe');
13 |
14 | exports.createSubscription = async (req, res, next) => {
15 | const {
16 | planId = 'stripe-plan-basic',
17 | paymentMethod
18 | } = req.body;
19 |
20 | let user = req.user;
21 |
22 |
23 | const email = user.email;
24 | const userId = user.id;
25 | let paymentMethodId;
26 |
27 | if (!paymentMethod) {
28 | // Get paymentMethod from user object
29 | paymentMethodId = _.get(user, 'stripeDetails.customer.invoice_settings.default_payment_method');
30 | } else {
31 | paymentMethodId = paymentMethod.id;
32 | }
33 |
34 | if (!paymentMethodId) {
35 | res.status(422).send({
36 | message: 'No payment method available for user'
37 | });
38 | return;
39 | }
40 |
41 | // Step 1: Get stripe customer from database
42 | let stripeCustomer = user.stripeDetails && user.stripeDetails.customer;
43 | // If there is no stripe customer
44 | if (!stripeCustomer) {
45 | console.log('Creating Customer');
46 | // Then create a new record
47 | stripeCustomer = await createCustomer(paymentMethodId, email);
48 | // Save new customer to the database
49 | user = await User.saveStripeCustomer(userId, stripeCustomer);
50 | } else {
51 | console.log('Using Existing Customer');
52 | }
53 |
54 |
55 | // Step 1: Get stripe Subscription from database
56 | let stripeSubscription = user.stripeDetails && user.stripeDetails.subscription;
57 | // If there is no stripe Subscription
58 | if (!stripeSubscription) {
59 | console.log('Creating Subscription');
60 | stripeSubscription = await createSubscription(stripeCustomer.id, planId);
61 | user = await User.saveStripeSubscription(userId, stripeSubscription);
62 | } else {
63 | console.log('Using Existing Subscription');
64 | const subscriptionId = stripeSubscription.id;
65 | // Update subscription with new plan
66 | stripeSubscription = await updateSubscription(subscriptionId, planId);
67 | user = await User.saveStripeSubscription(userId, stripeSubscription);
68 | }
69 |
70 |
71 |
72 |
73 | console.log('Created Subscription');
74 | console.log(stripeSubscription);
75 |
76 | res.send({
77 | subscription: stripeSubscription
78 | })
79 | }
80 |
81 | exports.getSubscription = async (req, res, next) => {
82 | const user = req.user;
83 |
84 | const subscription = user.stripeDetails && user.stripeDetails.subscription;
85 |
86 | res.send({
87 | subscription
88 | })
89 | }
90 |
91 |
92 | exports.getCard = async (req, res, next) => {
93 | const user = req.user;
94 |
95 | const stripeDetails = user.stripeDetails;
96 | const defaultPaymentMethod = _.get(stripeDetails, 'customer.invoice_settings.default_payment_method');
97 |
98 | if (defaultPaymentMethod) {
99 | const paymentMethod = await retrievePaymentMethod(defaultPaymentMethod);
100 | res.send({
101 | card: _.get(paymentMethod, 'card')
102 | });
103 | return;
104 | }
105 |
106 | res.send({
107 | card: null
108 | })
109 | }
110 |
111 | exports.updateCard = async (req, res, next) => {
112 | const {
113 | paymentMethod
114 | } = req.body;
115 |
116 | if (!paymentMethod) {
117 | res.status(422).send({
118 | message: 'Payment Method is required'
119 | });
120 | return;
121 | }
122 |
123 | let user = req.user;
124 | let paymentMethodId = paymentMethod.id;
125 | let customerId = _.get(user, 'stripeDetails.customer.id');
126 | let updatedPaymethod;
127 | let updatedCustomer;
128 |
129 | try {
130 | // Step 1: Attach payment method to customer
131 | updatedPaymethod = await attachPaymentMethodToCustomer(paymentMethodId, customerId);
132 | // Step 2: Update Customer and set payment method as default payment method
133 | updatedCustomer = await updateCustomer(customerId, {
134 | invoice_settings: {
135 | default_payment_method: paymentMethodId
136 | }
137 | })
138 | // Step 3: Update our database with new customer information
139 | user = await User.saveStripeCustomer(user.id, updatedCustomer);
140 |
141 | } catch (e) {
142 | console.log('e', e);
143 | res.status(500).send({
144 | code: 'GLOBAL_ERROR',
145 | field: '',
146 | message: 'An occurred while updating your card'
147 | });
148 | return;
149 | }
150 |
151 | res.send({
152 | customer: updatedCustomer
153 | })
154 |
155 | }
156 |
157 | exports.deleteSubscription = async (req, res, next) => {
158 | let user = req.user;
159 | const subscriptionId = _.get(user, 'stripeDetails.subscription.id');
160 |
161 | if (!subscriptionId) {
162 | res.status(422).send({
163 | message: 'You do not have any active subscriptions'
164 | });
165 | return;
166 | }
167 |
168 | let deletedSubscription;
169 |
170 | try {
171 | deletedSubscription = await deleteSubscription(subscriptionId);
172 | user = await User.saveStripeSubscription(user.id, null);
173 |
174 | console.log('deletedSubscription', deletedSubscription);
175 | } catch (e) {
176 | res.status(500).send({
177 | code: 'GLOBAL_ERROR',
178 | field: '',
179 | message: 'An occurred while cancelling your subscription'
180 | });
181 | return;
182 | }
183 |
184 | res.send({
185 | subscription: deletedSubscription
186 | })
187 |
188 | }
189 |
190 | exports.processStripeWebhook = async (req, res, next) => {
191 | const sig = req.headers['stripe-signature'];
192 | const endpointSecret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET;
193 |
194 | let event;
195 |
196 | try {
197 | event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
198 | }
199 | catch (err) {
200 | res.status(400).send(`Webhook Error: ${err.message}`);
201 | return;
202 | }
203 |
204 | switch (event.type) {
205 | case 'customer.subscription.updated':
206 | console.log('Received customer.subscription.updated');
207 | console.log('event', event);
208 | const subscription = event.data.object;
209 | const customer = 'cus_GiK4wRu8IyTFoK'; // subscription.customer;
210 |
211 | let user = await User.findOne({
212 | 'stripeDetails.customer.id': customer
213 | });
214 |
215 | if (user) {
216 | // Save subscription to the database
217 | console.log('user.toObject()', user.toObject());
218 | user = await User.saveStripeSubscription(user._id, subscription)
219 | }
220 |
221 | break;
222 |
223 | }
224 |
225 | // Return a response to acknowledge receipt of the event
226 | res.json({
227 | received: true
228 | });
229 | }
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
--------------------------------------------------------------------------------
/server/db/databaseSetup.js:
--------------------------------------------------------------------------------
1 | // connect to DB
2 |
3 | const mongoose = require('mongoose');
4 | mongoose.Promise = Promise;
5 |
6 | mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true })
7 | .then(() => {
8 | console.log('SUCCESS: DB connection ');
9 | })
10 | .catch((err) => {
11 | console.log('ERROR: DB connection ');
12 | console.log('err ', err);
13 | });
14 |
--------------------------------------------------------------------------------
/server/lib/EmailManager.js:
--------------------------------------------------------------------------------
1 | const mailgun = require("mailgun-js");
2 | const DOMAIN = process.env.MAILGUN_DOMAIN;
3 | const APP_EMAIL = process.env.APP_EMAIL;
4 |
5 | exports.sendAccountActivationEmail = async (user) => {
6 |
7 | const token = user.activationToken;
8 | // Eg: http://localhost:3000/account/activate?token=e04ce280-1ca8-11ea-b82f-dbd64ede476e
9 | const accountActivationLink = process.env.BASE_URL + '/account/activate?token=' + token;
10 | console.log('accountActivationLink', accountActivationLink);
11 |
12 | // Next, send this link via mailgun to the user
13 | const mg = mailgun({
14 | apiKey: process.env.MAILGUN_API_KEY,
15 | domain: DOMAIN
16 | });
17 |
18 |
19 | const data = {
20 | from: APP_EMAIL,
21 | to: process.env.NODE_ENV === 'production' ? user.email : APP_EMAIL,
22 | subject: 'Account Activation',
23 | template: "account_activation",
24 | 'h:X-Mailgun-Variables': JSON.stringify({
25 | activation_link: accountActivationLink
26 | })
27 | };
28 |
29 |
30 | try {
31 | const body = await mg.messages().send(data);
32 | console.log('body', body);
33 |
34 | } catch (e) {
35 | console.log('e', e);
36 | }
37 | }
38 |
39 | exports.resendActivationLink = async (user) => {
40 | return exports.sendAccountActivationEmail(user);
41 | }
42 |
43 |
44 | exports.sendResetPasswordLinkEmail = async (user) => {
45 |
46 | const token = user.resetPasswordToken;
47 | // Eg: http://localhost:3000/account/reset-password?token=e04ce280-1ca8-11ea-b82f-dbd64ede476e
48 | const resetPasswordLink = process.env.BASE_URL + '/account/reset-password?token=' + token;
49 | console.log('resetPasswordLink', resetPasswordLink);
50 |
51 | // Next, send this link via mailgun to the user
52 | const mg = mailgun({
53 | apiKey: process.env.MAILGUN_API_KEY,
54 | domain: DOMAIN
55 | });
56 |
57 |
58 | const data = {
59 | from: APP_EMAIL,
60 | to: process.env.NODE_ENV === 'production' ? user.email : APP_EMAIL,
61 | subject: 'Reset Password',
62 | template: "reset_password_link",
63 | 'h:X-Mailgun-Variables': JSON.stringify({
64 | reset_password_link: resetPasswordLink
65 | })
66 | };
67 |
68 |
69 | try {
70 | const body = await mg.messages().send(data);
71 | console.log('body', body);
72 |
73 | } catch (e) {
74 | console.log('e', e);
75 | }
76 | }
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/server/lib/StripeManager.js:
--------------------------------------------------------------------------------
1 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2 |
3 | exports.createCustomer = async (paymentMethodId, email) => {
4 | const customer = await stripe.customers.create({
5 | payment_method: paymentMethodId,
6 | email: email,
7 | invoice_settings: {
8 | default_payment_method: paymentMethodId,
9 | },
10 | });
11 |
12 | return customer;
13 | }
14 |
15 | exports.createSubscription = async (customerId, planId) => {
16 | const subscription = await stripe.subscriptions.create({
17 | customer: customerId,
18 | items: [{ plan: planId }],
19 | expand: ["latest_invoice.payment_intent"]
20 | });
21 |
22 | return subscription;
23 | }
24 |
25 | exports.updateSubscription = async (subscriptionId, newPlanId) => {
26 | const subscription = await stripe.subscriptions.retrieve(subscriptionId);
27 | const updatedSubscription = stripe.subscriptions.update(subscriptionId, {
28 | items: [{
29 | id: subscription.items.data[0].id,
30 | plan: newPlanId
31 | }]
32 | });
33 |
34 | return updatedSubscription;
35 | }
36 |
37 | exports.retrievePaymentMethod = async (paymentMethodId) => {
38 | const paymentMethod = await stripe.paymentMethods.retrieve(paymentMethodId);
39 |
40 | return paymentMethod;
41 | }
42 |
43 | exports.deleteSubscription = async (subscriptionId) => {
44 | const deletedSubscription = await stripe.subscriptions.del(subscriptionId)
45 | return deletedSubscription;
46 | }
47 |
48 | exports.attachPaymentMethodToCustomer = async (paymentMethodId, customerId) => {
49 | const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
50 | customer: customerId
51 | });
52 | return paymentMethod;
53 | }
54 |
55 | exports.updateCustomer = async (customerId, customerData) => {
56 | const updatedCustomer = await stripe.customers.update(customerId, customerData);
57 | return updatedCustomer;
58 | }
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/server/lib/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport');
2 | const LocalStrategy = require('passport-local').Strategy;
3 | const JwtStrategy = require('passport-jwt').Strategy;
4 | const ExtractJwt = require('passport-jwt').ExtractJwt;
5 | const User = require('../models/User');
6 |
7 | const localOptions = {
8 | usernameField: 'email',
9 | passwordField: 'password'
10 | };
11 | const localLogin = new LocalStrategy(localOptions, function(email, password, done) {
12 |
13 | User.findOne({ email }, function (err, user) {
14 | if (err) {
15 | return done(err);
16 | }
17 |
18 | if (!user) {
19 | const errorObject = {
20 | error: true,
21 | errors: [{
22 | code: 'GLOBAL_ERROR',
23 | message: 'Your login details could not be verified. Please try again.'
24 | }]
25 | };
26 |
27 | // No user with that email address.. Cannot proceed
28 | done(null, null, errorObject);
29 |
30 | return;
31 | }
32 |
33 |
34 | // proceed with password validation
35 | user.comparePassword(password, function(err, isMatch) {
36 | if (err) {
37 | return done(err);
38 | }
39 |
40 | if (!isMatch) {
41 | const errorObject = {
42 | error: true,
43 | errors: [{
44 | code: 'GLOBAL_ERROR',
45 | message: 'Your login details could not be verified. Please try again.'
46 | }]
47 | };
48 |
49 | done(null, null, errorObject);
50 |
51 | return;
52 | }
53 |
54 | done(null, user);
55 | return;
56 |
57 | })
58 |
59 |
60 |
61 | });
62 | });
63 |
64 |
65 | const jwtOpts = {}
66 | // jwtOpts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
67 |
68 | jwtOpts.jwtFromRequest = (req) => {
69 | const cookies = req.cookies;
70 | const token = cookies.token;
71 |
72 | if (token) {
73 | return token;
74 | }
75 |
76 | const headers = req.headers || {};
77 | const authHeader = headers.authorization || '';
78 | const headerToken = authHeader.split(' ')[1];
79 | if (headerToken) {
80 | return headerToken;
81 | }
82 |
83 |
84 | return null;
85 | }
86 |
87 | jwtOpts.secretOrKey = process.env.JWT_SECRET || 'TEMP_JWT_SECRET';
88 |
89 | passport.use(new JwtStrategy(jwtOpts, function(jwtPayload, done) {
90 | const userId = jwtPayload._id;
91 |
92 | User.findOne({ _id: userId }, function(err, user) {
93 | if (err) {
94 | return done(err, false);
95 | }
96 | if (user) {
97 | return done(null, user);
98 | } else {
99 | return done(null, false, {
100 | code: 'GLOBAL_ERROR',
101 | message: 'Email not associated with any account'
102 | });
103 | }
104 | });
105 | }));
106 |
107 |
108 | passport.use(localLogin);
109 |
110 |
111 |
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 | const bcrypt = require('bcrypt');
4 |
5 |
6 | let User;
7 |
8 | if (!User) {
9 | let userSchema = new Schema({
10 | email: { type: String, required: true, lowercase: true, unique: true },
11 | password: { type: String, required: true },
12 |
13 | // Fields related to account activation
14 | activated: { type: Boolean },
15 | activationToken: { type: String, unique: true, sparse: true },
16 | activationTokenSentAt: { type: Date },
17 | activatedAt: { type: Date },
18 |
19 | // Fields related to reset password
20 | resetPasswordToken: { type: String, unique: true, sparse: true },
21 | resetPasswordTokenSentAt: { type: Date },
22 |
23 | // Stripe Payments related
24 | stripeDetails: {}
25 | },
26 | {
27 | timestamps: true
28 | });
29 |
30 |
31 | userSchema.pre('save', function(next) {
32 | // this
33 | const user = this;
34 |
35 | const SALT_FACTOR = 5;
36 |
37 | if (!user.isModified('password')) {
38 | return next();
39 | }
40 |
41 | bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
42 | if (err) {
43 | return next(err);
44 | }
45 |
46 | bcrypt.hash(user.password, salt, function(err, hash) {
47 | if (err) {
48 | return next(err);
49 | }
50 | user.password = hash;
51 |
52 | next();
53 | })
54 | })
55 | });
56 |
57 |
58 | userSchema.methods.comparePassword = function(candidatePassword, callback) {
59 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
60 | if (err) {
61 | callback(err);
62 | return;
63 | }
64 |
65 | callback(null, isMatch);
66 | })
67 | }
68 |
69 | userSchema.statics.toClientObject = function(user) {
70 | const userObject = user.toObject() || {};
71 |
72 | const clientObject = {
73 | _id: userObject._id,
74 | email: userObject.email,
75 | activated: userObject.activated,
76 | createdAt: userObject.createdAt,
77 | updatedAt: userObject.updatedAt,
78 | stripeDetails: userObject.stripeDetails
79 | };
80 |
81 | return clientObject;
82 | }
83 |
84 | userSchema.statics.saveStripeCustomer = function(id, customer) {
85 | const updatedUser = this.findOneAndUpdate(
86 | { _id: id },
87 | { 'stripeDetails.customer': customer },
88 | { new: true }
89 | );
90 |
91 | return updatedUser;
92 | }
93 |
94 | userSchema.statics.saveStripeSubscription = function(id, subscription) {
95 | const updatedUser = this.findOneAndUpdate(
96 | { _id: id },
97 | { 'stripeDetails.subscription': subscription },
98 | { new: true }
99 | );
100 |
101 | return updatedUser;
102 | }
103 |
104 | User = mongoose.model('User', userSchema);
105 | }
106 |
107 | module.exports = User;
108 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "saas-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcrypt": "^3.0.7",
14 | "body-parser": "^1.19.0",
15 | "compression": "^1.7.4",
16 | "cookie-parser": "^1.4.4",
17 | "dotenv": "^8.2.0",
18 | "express": "^4.17.1",
19 | "jsonwebtoken": "^8.5.1",
20 | "lodash": "^4.17.15",
21 | "mailgun-js": "^0.22.0",
22 | "mongoose": "^5.7.12",
23 | "passport": "^0.4.0",
24 | "passport-jwt": "^4.0.0",
25 | "passport-local": "^1.0.0",
26 | "stripe": "^8.6.0",
27 | "uuid": "^3.3.3"
28 | },
29 | "engines" : {
30 | "node" : ">=10.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/routes.js:
--------------------------------------------------------------------------------
1 | const authController = require('./controllers/authController');
2 | const paymentsController = require('./controllers/paymentsController');
3 | const passport = require('passport');
4 | const checkAuth = passport.authenticate('jwt', { session: false });
5 | const bodyParser = require('body-parser');
6 |
7 | function addRoutes(app) {
8 | app.all('*', (req, res, next) => {
9 | console.log(req.method + ' ' + req.url);
10 | next();
11 | });
12 |
13 | app.get('/test-url', (req, res, next) => {
14 | res.send({
15 | success: true
16 | });
17 | })
18 |
19 | app.get('/api/logged-in-user', checkAuth, authController.getLoggedInUser);
20 |
21 |
22 | app.post('/api/register', authController.register);
23 | app.post('/api/login', authController.login);
24 | app.post('/api/account-activate', authController.accountActivate);
25 | app.post('/api/resend-activation-link', authController.resendActivationLink);
26 | app.post('/api/reset-password-link', authController.resetPasswordLink);
27 | app.post('/api/reset-password', authController.resetPassword);
28 |
29 | app.get('/api/test-auth', checkAuth , authController.testAuth);
30 |
31 | // Payments
32 | app.post('/api/payments/subscriptions', checkAuth, paymentsController.createSubscription );
33 | app.delete('/api/payments/subscriptions', checkAuth, paymentsController.deleteSubscription );
34 | app.get('/api/payments/subscriptions', checkAuth, paymentsController.getSubscription );
35 | app.get('/api/payments/cards', checkAuth, paymentsController.getCard );
36 | app.post('/api/payments/cards', checkAuth, paymentsController.updateCard );
37 |
38 | // Stripe Webhooks
39 | app.post('/api/stripe/webhooks',
40 | bodyParser.raw({
41 | type: 'application/json'
42 | }),
43 | paymentsController.processStripeWebhook
44 | );
45 |
46 | }
47 |
48 | const routes = {
49 | addRoutes
50 | };
51 |
52 | module.exports = routes;
53 |
54 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const express = require('express');
3 | const compression = require('compression');
4 | const bodyParser = require('body-parser');
5 | const cookieParser = require('cookie-parser');
6 | const routes = require('./routes');
7 | // connect to database on startup
8 | const databaseSetup = require('./db/databaseSetup');
9 | const passport = require('./lib/passport');
10 | const path = require('path');
11 | const app = express();
12 |
13 | app.use(compression());
14 | app.use(bodyParser.json({
15 | limit: '5mb',
16 |
17 | verify: (req, res, buf) => {
18 | if (req.originalUrl === '/api/stripe/webhooks') {
19 | req.rawBody = buf;
20 | }
21 | }
22 | }));
23 | app.use(cookieParser());
24 |
25 | routes.addRoutes(app);
26 |
27 | if (process.env.NODE_ENV === 'production') {
28 | app.use(express.static(path.join(__dirname, '../client/build')));
29 | app.get('*', (req, res, next) => {
30 | res.sendFile(path.resolve(__dirname, '../client/build/index.html'))
31 | });
32 | }
33 |
34 | app.listen(process.env.PORT || 3001, () => {
35 | console.log('Express Server started on PORT: ', process.env.PORT || 3001);
36 | });
37 |
38 | process.on('uncaughtException', (error) => {
39 | console.log('uncaughtException');
40 | console.log('error ', error);
41 | });
42 |
--------------------------------------------------------------------------------
/xscreenshots/1.HomePage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/1.HomePage.png
--------------------------------------------------------------------------------
/xscreenshots/2.Register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/2.Register.png
--------------------------------------------------------------------------------
/xscreenshots/3.Login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/3.Login.png
--------------------------------------------------------------------------------
/xscreenshots/4.ResendActivationLink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/4.ResendActivationLink.png
--------------------------------------------------------------------------------
/xscreenshots/5.ResetPasswordLink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/5.ResetPasswordLink.png
--------------------------------------------------------------------------------
/xscreenshots/6.ResetPassword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/6.ResetPassword.png
--------------------------------------------------------------------------------
/xscreenshots/7.Payments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saas-developer/saasify/f8ce66aa36cf96008d274507a0975528d87341be/xscreenshots/7.Payments.png
--------------------------------------------------------------------------------