├── .github └── FUNDING.yml ├── project ├── client │ ├── .travis.yml │ ├── src │ │ ├── .eslintrc │ │ ├── components │ │ │ ├── Navigation │ │ │ │ ├── index.js │ │ │ │ └── NavbarOne.jsx │ │ │ ├── Footer │ │ │ │ ├── index.js │ │ │ │ └── FooterOne.jsx │ │ │ ├── Hero │ │ │ │ ├── Hero.jsx │ │ │ │ └── HeroOne.jsx │ │ │ ├── CallToAction │ │ │ │ └── CallToActionOne.jsx │ │ │ ├── PasswordReset │ │ │ │ └── Password.jsx │ │ │ ├── Pricing │ │ │ │ ├── PricingSectionOne.jsx │ │ │ │ └── PlanCardOne.jsx │ │ │ ├── Benefits │ │ │ │ └── BenefitsOne.jsx │ │ │ ├── SignInForm │ │ │ │ └── SignInFormOne.jsx │ │ │ └── SignUpForm │ │ │ │ └── SignUpFormOne.jsx │ │ ├── constants │ │ │ ├── urls.js │ │ │ └── routes.js │ │ ├── lib │ │ │ ├── Auth │ │ │ │ ├── index.js │ │ │ │ ├── auth.js │ │ │ │ └── context.jsx │ │ │ ├── Firebase │ │ │ │ ├── index.js │ │ │ │ ├── context.jsx │ │ │ │ └── firebase.js │ │ │ ├── withLoadingSpinner.jsx │ │ │ ├── withStaq.jsx │ │ │ ├── StaqStyleProvider.jsx │ │ │ ├── withStripe.jsx │ │ │ ├── signup.js │ │ │ └── StaqRoutes.jsx │ │ ├── styles.module.css │ │ ├── pages │ │ │ ├── SignUpPage │ │ │ │ ├── SignUpPageOne.jsx │ │ │ │ └── SignUpPage.jsx │ │ │ ├── SignInPage │ │ │ │ ├── SignInPageOne.jsx │ │ │ │ └── SignInPage.jsx │ │ │ ├── PricingPage │ │ │ │ ├── PricingPageOne.jsx │ │ │ │ └── PricingPage.jsx │ │ │ ├── ForgotPasswordPage │ │ │ │ ├── ForgotPasswordPage.jsx │ │ │ │ └── ForgotPasswordPageOne.jsx │ │ │ └── LandingPage │ │ │ │ ├── LandingPage.js │ │ │ │ ├── LandingPageOne.jsx │ │ │ │ └── LandingPageTwo.jsx │ │ └── index.js │ ├── postcss.config.js │ ├── styles │ │ └── tailwind.css │ ├── landing-page.png │ ├── babel.config.json │ ├── .editorconfig │ ├── jsconfig.json │ ├── .prettierrc │ ├── tailwind.config.js │ ├── .eslintrc.json │ ├── rollup.config.js │ ├── package.json │ └── tailwind.js ├── server │ ├── util.js │ ├── index.js │ ├── functions │ │ ├── createStripeCustomerPortalSession.js │ │ ├── createStripeCheckoutSession.js │ │ ├── onStripeCheckoutSessionCompleted.js │ │ └── stripeCustomer.js │ └── package.json └── staq.js ├── jsconfig.json ├── .gitignore ├── LICENSE ├── CODE_OF_CONDUCT.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mroll 2 | 3 | 4 | -------------------------------------------------------------------------------- /project/client/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /project/client/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /project/client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss')], 3 | } 4 | -------------------------------------------------------------------------------- /project/client/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /project/client/landing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staqjs/staq/HEAD/project/client/landing-page.png -------------------------------------------------------------------------------- /project/client/src/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import NavbarOne from './NavbarOne' 2 | 3 | export { NavbarOne } 4 | -------------------------------------------------------------------------------- /project/client/src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import FooterOne from './FooterOne' 2 | 3 | export { 4 | FooterOne 5 | } 6 | -------------------------------------------------------------------------------- /project/client/src/constants/urls.js: -------------------------------------------------------------------------------- 1 | export const STRIPE_CUSTOMER = '/customer' 2 | export const STRIPE_SUBSCRIPTION = '/subscription' 3 | -------------------------------------------------------------------------------- /project/client/src/lib/Auth/index.js: -------------------------------------------------------------------------------- 1 | import AuthProvider, { withAuth } from './context' 2 | 3 | export { 4 | AuthProvider, 5 | withAuth, 6 | } 7 | -------------------------------------------------------------------------------- /project/client/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-react-jsx", "@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /project/client/src/lib/Firebase/index.js: -------------------------------------------------------------------------------- 1 | import FirebaseContext, { withFirebase } from './context' 2 | import Firebase from './firebase' 3 | 4 | export default Firebase 5 | export { FirebaseContext, withFirebase } 6 | -------------------------------------------------------------------------------- /project/client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /project/client/src/styles.module.css: -------------------------------------------------------------------------------- 1 | /* add css module styles here (optional) */ 2 | 3 | .test { 4 | margin: 2em; 5 | padding: 0.5em; 6 | border: 2px solid #000; 7 | font-size: 2em; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowSyntheticDefaultImports": true, 5 | "noEmit": true, 6 | "checkJs": true, 7 | "jsx": "react", 8 | "lib": [ "dom", "es2017" ] 9 | } 10 | } -------------------------------------------------------------------------------- /project/client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowSyntheticDefaultImports": true, 5 | "noEmit": true, 6 | "checkJs": true, 7 | "jsx": "react", 8 | "lib": [ "dom", "es2017" ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /project/client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": false, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /project/client/src/constants/routes.js: -------------------------------------------------------------------------------- 1 | export const Landing = '/' 2 | export const SignUp = '/signup' 3 | export const SignIn = '/signin' 4 | export const Pricing = '/pricing' 5 | export const Demo = '/demo' 6 | export const UserSettings = '/settings/user' 7 | export const BillingSettings = '/settings/billing' 8 | export const ForgotPassword = '/forgot-password' 9 | -------------------------------------------------------------------------------- /project/client/src/lib/Firebase/context.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FirebaseContext = React.createContext(null) 4 | 5 | export const withFirebase = (Component) => (props) => ( 6 | 7 | {(firebase) => } 8 | 9 | ) 10 | 11 | export default FirebaseContext 12 | -------------------------------------------------------------------------------- /project/client/src/lib/withLoadingSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CgSpinner } from 'react-icons/cg' 3 | 4 | export function withLoadingSpinner(component, isLoading) { 5 | return function () { 6 | return isLoading ? ( 7 | 8 | ) : ( 9 | component 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .log -------------------------------------------------------------------------------- /project/client/src/components/Hero/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import HeroOne from './HeroOne' 3 | 4 | const getHeroComponent = (templateName) => { 5 | if (templateName === 'One') { 6 | return HeroOne 7 | } 8 | 9 | return HeroOne 10 | } 11 | 12 | function Hero(props) { 13 | const { templateName } = props 14 | const HeroComponent = getHeroComponent(templateName) 15 | 16 | return 17 | } 18 | 19 | export default Hero 20 | -------------------------------------------------------------------------------- /project/client/src/pages/SignUpPage/SignUpPageOne.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | 4 | import { signup } from '../../lib/signup' 5 | 6 | import SignUpFormOne from '../../components/SignUpForm/SignUpFormOne' 7 | 8 | function SignUpPageOne(props) { 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default SignUpPageOne 17 | -------------------------------------------------------------------------------- /project/client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 5 | plugins: [], 6 | prefix: 'sjs-', 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: colors.violet[600], 11 | }, 12 | maxWidth: { 13 | '1/4': '25%', 14 | '1/2': '50%', 15 | '3/4': '75%', 16 | '2/5': '40%', 17 | '3/5': '60%', 18 | }, 19 | }, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /project/server/util.js: -------------------------------------------------------------------------------- 1 | import staqConfig from '../staq' 2 | const { SecretManagerServiceClient } = require('@google-cloud/secret-manager') 3 | 4 | export const getSecret = async (secretName) => { 5 | const projectNumber = staqConfig.get('gcpProjectNumber') 6 | const secretPath = `projects/${projectNumber}/secrets/${secretName}/versions/latest` 7 | const smClient = new SecretManagerServiceClient() 8 | const [version] = await smClient.accessSecretVersion({ 9 | name: secretPath 10 | }) 11 | return version.payload.data.toString() 12 | } 13 | -------------------------------------------------------------------------------- /project/server/index.js: -------------------------------------------------------------------------------- 1 | import stripeCustomer from './functions/stripeCustomer' 2 | import createStripeCustomerPortalSession from './functions/createStripeCustomerPortalSession' 3 | import createStripeCheckoutSession from './functions/createStripeCheckoutSession' 4 | import onStripeCheckoutSessionCompleted from './functions/onStripeCheckoutSessionCompleted' 5 | import { initStaq } from '../staq' 6 | import { getSecret } from './util' 7 | 8 | export { 9 | getSecret, 10 | initStaq, 11 | createStripeCheckoutSession, 12 | createStripeCustomerPortalSession, 13 | onStripeCheckoutSessionCompleted, 14 | stripeCustomer, 15 | } 16 | -------------------------------------------------------------------------------- /project/client/src/lib/withStaq.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Firebase, { FirebaseContext } from './Firebase' 3 | import { AuthProvider } from './Auth' 4 | import StripeProvider from './withStripe' 5 | import staqConfig from '../../../staq' 6 | 7 | export default (children) => { 8 | const firebase = new Firebase(staqConfig.get('FirebaseConfig')) 9 | staqConfig.set('firebase', firebase) 10 | 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /project/client/src/pages/SignInPage/SignInPageOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import staqConfig from '../../../../staq' 5 | import { withAuth } from '../../lib/Auth' 6 | 7 | import SignInFormOne from '../../components/SignInForm/SignInFormOne' 8 | 9 | function SignInPageOne(props) { 10 | const { auth } = props 11 | const userHome = staqConfig.get('userHome') || '/' 12 | 13 | return auth.currentUser ? ( 14 | 15 | ) : ( 16 |
17 | 18 |
19 | ) 20 | } 21 | 22 | export default withAuth(SignInPageOne) 23 | -------------------------------------------------------------------------------- /project/server/functions/createStripeCustomerPortalSession.js: -------------------------------------------------------------------------------- 1 | import { getSecret } from '../util' 2 | 3 | const functions = require('firebase-functions') 4 | const _stripe = require("stripe") 5 | 6 | 7 | async function createPortalSession(data, context, stripe) { 8 | const session = await stripe.billingPortal.sessions.create({ 9 | customer: data.customerId, 10 | return_url: data.return_url 11 | }) 12 | 13 | return session 14 | } 15 | 16 | 17 | export default functions.https.onCall(async (data, context) => { 18 | console.log('data', data) 19 | 20 | const stripeSecretKey = await getSecret('stripe-secret-key') 21 | const stripe = _stripe(stripeSecretKey) 22 | 23 | return createPortalSession(data, context, stripe) 24 | }) 25 | -------------------------------------------------------------------------------- /project/client/src/pages/PricingPage/PricingPageOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import staqConfig from '../../../../staq' 4 | import * as ROUTES from '../../constants/routes' 5 | 6 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne' 7 | import CallToActionOne from '../../components/CallToAction/CallToActionOne' 8 | 9 | function PricingPageOne() { 10 | const pricingProps = staqConfig.get('Template.Config.Pricing', {}) 11 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {}) 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 | ) 19 | } 20 | 21 | export default PricingPageOne 22 | -------------------------------------------------------------------------------- /project/client/src/components/CallToAction/CallToActionOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | function CallToActionOne(props) { 5 | const { Title, ActionText, ActionLink } = props 6 | 7 | return ( 8 |
9 |

{Title}

10 | 11 |
12 | 18 | {ActionText} 19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default CallToActionOne 26 | -------------------------------------------------------------------------------- /project/client/src/pages/SignInPage/SignInPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import staqConfig from '../../../../staq' 5 | import { withAuth } from '../../lib/Auth' 6 | 7 | import SignInPageOne from './SignInPageOne' 8 | 9 | const getSignInPageComponent = () => { 10 | const layoutName = staqConfig.get('Template') 11 | if (layoutName === 'One') { 12 | return SignInPageOne 13 | } 14 | 15 | return SignInPageOne 16 | } 17 | 18 | function SignInPage(props) { 19 | const { auth } = props 20 | const SignInPageComponent = getSignInPageComponent() 21 | 22 | return auth.currentUser ? ( 23 | 24 | ) : ( 25 | 26 | ) 27 | } 28 | 29 | export default withAuth(SignInPage) 30 | -------------------------------------------------------------------------------- /project/client/src/pages/SignUpPage/SignUpPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import staqConfig from '../../../../staq' 5 | import { withAuth } from '../../lib/Auth' 6 | 7 | import SignUpPageOne from './SignUpPageOne' 8 | 9 | const getSignUpPageComponent = () => { 10 | const layoutName = staqConfig.get('Template') 11 | if (layoutName === 'One') { 12 | return SignUpPageOne 13 | } 14 | 15 | return SignUpPageOne 16 | } 17 | 18 | function SignUpPage(props) { 19 | const { auth } = props 20 | const SignUpPageComponent = getSignUpPageComponent() 21 | 22 | return auth.currentUser ? ( 23 | 24 | ) : ( 25 | 26 | ) 27 | } 28 | 29 | export default withAuth(SignUpPage) 30 | -------------------------------------------------------------------------------- /project/client/src/pages/PricingPage/PricingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import staqConfig from '../../../../staq' 5 | import { withAuth } from '../../lib/Auth' 6 | 7 | import PricingPageOne from './PricingPageOne' 8 | 9 | const getPricingPageComponent = () => { 10 | const layoutName = staqConfig.get('Template') 11 | if (layoutName === 'One') { 12 | return PricingPageOne 13 | } 14 | 15 | return PricingPageOne 16 | } 17 | 18 | function PricingPage(props) { 19 | const { auth } = props 20 | const PricingPageComponent = getPricingPageComponent() 21 | 22 | return auth.currentUser ? ( 23 | 24 | ) : ( 25 | 26 | ) 27 | } 28 | 29 | export default withAuth(PricingPage) 30 | -------------------------------------------------------------------------------- /project/client/src/components/PasswordReset/Password.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Password(props) { 4 | const { sendPasswordResetEmail, passwordResetMessage } = props 5 | 6 | const handleSubmit = (event) => { 7 | event.preventDefault() 8 | sendPasswordResetEmail() 9 | } 10 | 11 | return ( 12 |
13 |

Password Reset

14 |
15 |
16 | 19 | 20 | {passwordResetMessage ? ( 21 | {passwordResetMessage} 22 | ) : null} 23 |
24 |
25 |
26 | ) 27 | } 28 | 29 | export default Password 30 | -------------------------------------------------------------------------------- /project/client/src/lib/StaqStyleProvider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | StylesProvider, 4 | createGenerateClassName, 5 | createMuiTheme 6 | } from '@material-ui/core/styles' 7 | import { ThemeProvider } from '@material-ui/styles' 8 | 9 | import staqConfig from '../../../staq' 10 | 11 | const generateClassName = createGenerateClassName({ 12 | disableGlobal: true, 13 | productionPrefix: 'staq', 14 | seed: 'staq' 15 | }) 16 | 17 | const defaultTheme = createMuiTheme() 18 | 19 | export default function StaqStyleProvider(props) { 20 | const { children } = props 21 | const theme = staqConfig.get('Theme') || defaultTheme 22 | 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /project/client/src/pages/ForgotPasswordPage/ForgotPasswordPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | import staqConfig from '../../../../staq' 5 | import { withAuth } from '../../lib/Auth' 6 | 7 | import ForgotPasswordPageOne from './ForgotPasswordPageOne' 8 | 9 | const getForgotPasswordPageComponent = () => { 10 | const layoutName = staqConfig.get('Template') 11 | if (layoutName === 'One') { 12 | return ForgotPasswordPageOne 13 | } 14 | 15 | return ForgotPasswordPageOne 16 | } 17 | 18 | function ForgotPasswordPage(props) { 19 | const { auth } = props 20 | const ForgotPasswordPageComponent = getForgotPasswordPageComponent() 21 | 22 | return auth.currentUser ? ( 23 | 24 | ) : ( 25 | 26 | ) 27 | } 28 | 29 | export default withAuth(ForgotPasswordPage) 30 | -------------------------------------------------------------------------------- /project/client/src/pages/LandingPage/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Redirect } from 'react-router-dom' 4 | 5 | import staqConfig from '../../../../staq' 6 | import { withAuth } from '../../lib/Auth' 7 | 8 | import LandingPageOne from './LandingPageOne' 9 | import LandingPageTwo from './LandingPageTwo' 10 | 11 | const getLandingPageComponent = () => { 12 | const layoutName = staqConfig.get('Template') 13 | if (layoutName === 'One') { 14 | return LandingPageOne 15 | } 16 | 17 | if (layoutName === 'Two') { 18 | return LandingPageTwo 19 | } 20 | 21 | return LandingPageOne 22 | } 23 | 24 | function LandingPage(props) { 25 | const { auth } = props 26 | const LandingPageComponent = getLandingPageComponent() 27 | 28 | return auth.currentUser ? ( 29 | 30 | ) : ( 31 | 32 | ) 33 | } 34 | 35 | export default withAuth(LandingPage) 36 | -------------------------------------------------------------------------------- /project/client/src/index.js: -------------------------------------------------------------------------------- 1 | import { initStaq } from '../../staq' 2 | 3 | // Components 4 | // NOTE: Not exporting components for now. 5 | // Export pages built from components. 6 | // Will export components in the future. 7 | // ... 8 | 9 | // Pages 10 | import LandingPage from './pages/LandingPage/LandingPage' 11 | 12 | // Components 13 | import Hero from './components/Hero/Hero' 14 | 15 | // Lib 16 | import withStaq from './lib/withStaq' 17 | import StaqRoutes, { PrivateRoute } from './lib/StaqRoutes' 18 | import { getStripeCheckoutSession, withStripe } from './lib/withStripe' 19 | import { withFirebase } from './lib/Firebase' 20 | import { withLoadingSpinner } from './lib/withLoadingSpinner' 21 | import { withAuth } from './lib/Auth' 22 | 23 | export { 24 | PrivateRoute, 25 | StaqRoutes, 26 | LandingPage, 27 | Hero, 28 | initStaq, 29 | withAuth, 30 | withFirebase, 31 | withLoadingSpinner, 32 | withStaq, 33 | withStripe, 34 | getStripeCheckoutSession, 35 | } 36 | -------------------------------------------------------------------------------- /project/server/functions/createStripeCheckoutSession.js: -------------------------------------------------------------------------------- 1 | import { getSecret } from '../util' 2 | 3 | const functions = require('firebase-functions') 4 | const _stripe = require("stripe") 5 | 6 | 7 | async function createCheckoutSession(data, context, stripe) { 8 | const session = await stripe.checkout.sessions.create({ 9 | payment_method_types: ["card"], 10 | mode: "payment", 11 | customer: data.customerId, 12 | client_reference_id: data.clientReferenceId, 13 | line_items: [ 14 | { 15 | price: data.priceId, 16 | quantity: 1 17 | } 18 | ], 19 | success_url: data.successUrl, 20 | cancel_url: data.cancelUrl, 21 | }) 22 | 23 | return session 24 | } 25 | 26 | 27 | export default functions.https.onCall(async (data, context) => { 28 | console.log('data', data) 29 | 30 | const stripeSecretKey = await getSecret('stripe-secret-key') 31 | const stripe = _stripe(stripeSecretKey) 32 | 33 | return createCheckoutSession(data, context, stripe) 34 | }) 35 | -------------------------------------------------------------------------------- /project/client/src/pages/LandingPage/LandingPageOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import staqConfig from '../../../../staq' 4 | 5 | import HeroOne from '../../components/Hero/HeroOne' 6 | import BenefitsOne from '../../components/Benefits/BenefitsOne' 7 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne' 8 | import CallToActionOne from '../../components/CallToAction/CallToActionOne' 9 | 10 | function LandingPageOne() { 11 | const heroProps = staqConfig.get('Template.Config.Hero', {}) 12 | const benefitsProps = staqConfig.get('Template.Config.Benefits', []) 13 | const pricingProps = staqConfig.get('Template.Config.Pricing', {}) 14 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {}) 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 |
23 | ) 24 | } 25 | 26 | export default LandingPageOne 27 | -------------------------------------------------------------------------------- /project/staq.js: -------------------------------------------------------------------------------- 1 | // Define this so we don't need to use lodash 2 | // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get 3 | const _get = (obj, path, defaultValue = undefined) => { 4 | const travel = (regexp) => 5 | String.prototype.split 6 | .call(path, regexp) 7 | .filter(Boolean) 8 | .reduce( 9 | (res, key) => (res !== null && res !== undefined ? res[key] : res), 10 | obj 11 | ); 12 | const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/); 13 | return result === undefined || result === obj ? defaultValue : result; 14 | }; 15 | 16 | class StaqConfig { 17 | constructor() { 18 | this.config = {}; 19 | } 20 | 21 | setConfig(config) { 22 | this.config = config; 23 | } 24 | 25 | get(field, dflt) { 26 | return _get(this.config, field, dflt); 27 | } 28 | 29 | set(field, value) { 30 | this.config[field] = value; 31 | } 32 | } 33 | 34 | const staqConfig = new StaqConfig(); 35 | 36 | export const initStaq = (config) => { 37 | staqConfig.config = config; 38 | }; 39 | 40 | export default staqConfig; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matt Roll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /project/client/src/pages/LandingPage/LandingPageTwo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'tailwindcss/tailwind.css' 3 | 4 | import staqConfig from '../../../../staq' 5 | 6 | import HeroOne from '../../components/Hero/HeroOne' 7 | import BenefitsOne from '../../components/Benefits/BenefitsOne' 8 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne' 9 | import CallToActionOne from '../../components/CallToAction/CallToActionOne' 10 | 11 | function LandingPageOne() { 12 | const classes = useStyles() 13 | 14 | const heroProps = staqConfig.get('Template.Config.Hero', {}) 15 | const benefitsProps = staqConfig.get('Template.Config.Benefits', {}) 16 | const pricingProps = staqConfig.get('Template.Config.Pricing', {}) 17 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {}) 18 | 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | 29 | export default LandingPageOne 30 | -------------------------------------------------------------------------------- /project/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@staqjs/server", 3 | "version": "0.0.8", 4 | "description": "StaqJS is a Javascript library for building Software-as-a-Service (SaaS) businesses.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.modern.js", 7 | "source": "index.js", 8 | "engines": { 9 | "node": ">=10" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "microbundle-crl --no-compress", 16 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "dependencies": { 20 | "@google-cloud/secret-manager": "^3.1.0", 21 | "firebase-admin": "^9.0.0", 22 | "firebase-functions": "^3.8.0", 23 | "microbundle-crl": "^0.13.11", 24 | "stripe": "^8.81.0" 25 | }, 26 | "devDependencies": {}, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/staqjs/staq.git" 30 | }, 31 | "author": "Matt Roll", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/staqjs/staq/issues" 35 | }, 36 | "homepage": "https://github.com/staqjs/staq#readme" 37 | } 38 | -------------------------------------------------------------------------------- /project/client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "airbnb" 9 | ], 10 | "parser": "babel-eslint", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 11, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | "semi": [ 23 | 2, 24 | "never" 25 | ], 26 | "no-console": [ 27 | 2, 28 | { 29 | "allow": [ 30 | "error", 31 | "warn" 32 | ] 33 | } 34 | ], 35 | "no-underscore-dangle": [ 36 | 2, 37 | { 38 | "allowAfterThis": true 39 | } 40 | ], 41 | "react/jsx-props-no-spreading": [ 42 | 2, 43 | { 44 | "custom": "ignore" 45 | } 46 | ], 47 | "react/prop-types": 0, 48 | "arrow-body-style": 0, 49 | "no-nested-ternary": 0, 50 | "no-multi-assign": 0 51 | }, 52 | "settings": { 53 | "import/resolver": { 54 | "node": { 55 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /project/client/src/components/Pricing/PricingSectionOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import PlanCardOne from './PlanCardOne' 4 | 5 | function PricingSectionOne(props) { 6 | const { Title, Subtitle, Plans } = props 7 | 8 | return ( 9 |
10 |
13 |

18 | {Title} 19 |

20 | 21 |
22 |

{Subtitle}

23 |
24 | 25 |
26 | {Plans.map((plan) => ( 27 | 35 | ))} 36 |
37 |
38 |
39 | ) 40 | } 41 | 42 | export default PricingSectionOne 43 | -------------------------------------------------------------------------------- /project/client/src/components/Benefits/BenefitsOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function BenefitsBox(props) { 4 | const { MiniTitle, Title, Message } = props 5 | 6 | return ( 7 |
8 |

{MiniTitle}

9 |

{Title}

10 |

{Message}

11 |
12 | ) 13 | } 14 | 15 | function BenefitsOne(props) { 16 | const { Benefits } = props 17 | 18 | return ( 19 |
20 |
21 |
22 |
27 | {Benefits.map((benefit) => ( 28 | 34 | ))} 35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | 42 | export default BenefitsOne 43 | -------------------------------------------------------------------------------- /project/client/src/lib/Auth/auth.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | class Auth { 4 | constructor(firebase) { 5 | this.firebase = firebase 6 | 7 | this.currentUser = JSON.parse(localStorage.getItem('currentUser')) 8 | this.onLogoutCallback = () => {} 9 | 10 | this.initListener() 11 | } 12 | 13 | initListener() { 14 | this.listener = this.firebase.onAuthUserListener( 15 | (currentUser) => { 16 | localStorage.setItem('currentUser', JSON.stringify(currentUser)) 17 | this.currentUser = currentUser 18 | }, 19 | () => { 20 | localStorage.removeItem('currentUser') 21 | this.currentUser = null 22 | this.onLogoutCallback() 23 | } 24 | ) 25 | } 26 | 27 | onLogout = (newCallbackOnLogout) => { 28 | this.onLogoutCallback = newCallbackOnLogout 29 | } 30 | 31 | reload = () => { 32 | this.currentUser = JSON.parse(localStorage.getItem('currentUser')) 33 | } 34 | 35 | update = (newFields) => { 36 | this 37 | .firebase.user(this.currentUser.uid) 38 | .update(newFields) 39 | .then(() => { 40 | const currentUser = _.merge({}, this.currentUser, newFields) 41 | localStorage.setItem('currentUser', JSON.stringify(currentUser)) 42 | this.currentUser = currentUser 43 | }) 44 | } 45 | } 46 | 47 | export default Auth 48 | -------------------------------------------------------------------------------- /project/server/functions/onStripeCheckoutSessionCompleted.js: -------------------------------------------------------------------------------- 1 | import staqConfig from "../../staq"; 2 | import { getSecret } from "../util"; 3 | 4 | const functions = require("firebase-functions"); 5 | const _stripe = require("stripe"); 6 | 7 | let endpointSecret; 8 | let fulfillOrder; 9 | 10 | async function onStripeCheckoutSessionCompleted(req, res, stripe) { 11 | const payload = req.rawBody; 12 | const sig = req.headers["stripe-signature"]; 13 | 14 | let event; 15 | 16 | try { 17 | event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); 18 | } catch (err) { 19 | console.error(err); 20 | return res.status(400).send(`Webhook Error: ${err.message}`); 21 | } 22 | 23 | if (event.type === "checkout.session.completed") { 24 | const session = event.data.object; 25 | fulfillOrder(session, () => { 26 | console.log("inside callback"); 27 | res.status(200).end(); 28 | }); 29 | } else { 30 | res.status(200).end(); 31 | } 32 | } 33 | 34 | export default functions.https.onRequest(async (req, res) => { 35 | const stripeSecretKey = await getSecret("stripe-secret-key"); 36 | const stripe = _stripe(stripeSecretKey); 37 | 38 | const secretName = staqConfig.get("stripeCheckoutSessionCompletedSecretName"); 39 | endpointSecret = await getSecret(secretName); 40 | 41 | fulfillOrder = staqConfig.get("stripeFulfillOrder"); 42 | 43 | onStripeCheckoutSessionCompleted(req, res, stripe); 44 | }); 45 | -------------------------------------------------------------------------------- /project/client/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import postcss from 'rollup-plugin-postcss' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | 6 | const extensions = ['.js', '.jsx', '.ts', '.tsx'] 7 | 8 | export default { 9 | input: './src/index.js', 10 | output: { 11 | file: './dist/index.js', 12 | format: 'esm', 13 | }, 14 | plugins: [ 15 | postcss({ 16 | config: { 17 | path: './postcss.config.js', 18 | }, 19 | extensions: ['.css'], 20 | minimize: true, 21 | inject: { 22 | insertAt: 'top', 23 | }, 24 | }), 25 | resolve({ 26 | mainFields: ['module', 'main', 'jsnext:main', 'browser'], 27 | extensions, 28 | }), 29 | babel({ 30 | exclude: './node_modules/**', 31 | extensions, 32 | }), 33 | commonjs({ 34 | include: 'node_modules/**', 35 | // left-hand side can be an absolute path, a path 36 | // relative to the current directory, or the name 37 | // of a module in node_modules 38 | namedExports: { 39 | 'node_modules/react/index.js': [ 40 | 'cloneElement', 41 | 'createContext', 42 | 'Component', 43 | 'createElement', 44 | ], 45 | 'node_modules/react-dom/index.js': ['render', 'hydrate'], 46 | 'node_modules/react-is/index.js': [ 47 | 'isFragment', 48 | 'isElement', 49 | 'isValidElementType', 50 | 'ForwardRef', 51 | 'Memo', 52 | ], 53 | }, 54 | }), 55 | ], 56 | external: [ 57 | 'react', 58 | 'react-dom', 59 | 'react-router-dom', 60 | 'firebase/app', 61 | 'firebase/analytics', 62 | 'firebase/auth', 63 | 'firebase/functions', 64 | 'firebase/firestore', 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /project/client/src/lib/withStripe.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { loadStripe } from '@stripe/stripe-js' 3 | import { Elements, useStripe } from '@stripe/react-stripe-js' 4 | import staqConfig from '../../../staq' 5 | 6 | const noop = () => {} 7 | 8 | const StripeContext = React.createContext(null) 9 | 10 | export const withStripe = (Component) => (props) => ( 11 | 12 | {(stripeContext) => } 13 | 14 | ) 15 | 16 | const StripeProviderBase = (props) => { 17 | const stripe = useStripe() 18 | const { children } = props 19 | return ( 20 | 25 | {children} 26 | 27 | ) 28 | } 29 | 30 | export const getStripeCheckoutSession = ( 31 | clientReferenceId, 32 | stripeCustomerId, 33 | priceId, 34 | ) => { 35 | const firebase = staqConfig.get('firebase') 36 | const successUrl = staqConfig.get('Payments.CheckoutSuccessUrl') 37 | const cancelUrl = staqConfig.get('Payments.CheckoutCancelUrl') 38 | const createCheckoutSession = firebase.functions.httpsCallable( 39 | 'createStripeCheckoutSession', 40 | ) 41 | return createCheckoutSession({ 42 | clientReferenceId, 43 | priceId, 44 | successUrl, 45 | cancelUrl, 46 | customerId: stripeCustomerId, 47 | }) 48 | } 49 | 50 | export default (props) => { 51 | const { children } = props 52 | const usePayments = staqConfig.get('Payments.Enabled') 53 | const stripePromise = usePayments 54 | ? loadStripe(staqConfig.get('Payments.StripePublishableKey')) 55 | : new Promise(noop) 56 | return ( 57 | 58 | {children} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /project/client/src/components/Pricing/PlanCardOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | function PlanCardOne(props) { 5 | const { Title, Subtitle, Price, Features, CtaLink } = props 6 | 7 | return ( 8 |
13 |
14 |

{Title}

15 |

{Subtitle}

16 | 17 |
18 |
19 | 20 | {' '} 21 | {Price.Value}{' '} 22 | 23 | 28 | {Price.Description}{' '} 29 | 30 |
31 |
{Price.Subdescription}
32 |
33 |
34 | 35 |
36 | 37 |
38 |
    39 | {Features.map((feature) => ( 40 |
  • 41 | {feature.Text} 42 |
  • 43 | ))} 44 |
45 | 46 | 52 | {CtaLink.Text} 53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | export default PlanCardOne 60 | -------------------------------------------------------------------------------- /project/client/src/components/Hero/HeroOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | function HeroOne(props) { 5 | const { 6 | PrimaryText, 7 | SecondaryText, 8 | Image, 9 | PrimaryLink, 10 | SecondaryLink, 11 | } = props 12 | 13 | return ( 14 |
15 |
20 |
21 |
22 | {PrimaryText} 23 |
24 | 25 |
26 | {SecondaryText} 27 |
28 | 29 |
30 | {PrimaryLink && ( 31 | 37 | {PrimaryLink.Text} 38 | 39 | )} 40 | 41 | {SecondaryLink && ( 42 | 48 | {SecondaryLink.Text} 49 | 50 | )} 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | ) 59 | } 60 | 61 | export default HeroOne 62 | -------------------------------------------------------------------------------- /project/server/functions/stripeCustomer.js: -------------------------------------------------------------------------------- 1 | import staqConfig from "../../staq"; 2 | import { getSecret } from "../util"; 3 | 4 | const functions = require("firebase-functions"); 5 | const _stripe = require("stripe"); 6 | 7 | async function createSubscription(customer) { 8 | try { 9 | const subscriptionObj = { 10 | customer: customer.id, 11 | items: [{ price: staqConfig.get("stripeTrialPriceId") }], 12 | }; 13 | 14 | if (staqConfig.get("useTrial")) { 15 | subscriptionObj.trial_period_days = 16 | staqConfig.get("stripeTrialPeriodDays") || 14; 17 | } 18 | 19 | const subscription = await stripe.subscriptions.create(subscriptionObj); 20 | 21 | return subscription; 22 | } catch (error) { 23 | console.error(error); 24 | throw new Error(error.message); 25 | } 26 | } 27 | 28 | async function createCustomer(data, context, stripe) { 29 | try { 30 | const customer = await stripe.customers.create(data.customer); 31 | 32 | const subscription = 33 | staqConfig.get("paymentType") === "subscription" 34 | ? createSubscription(customer) 35 | : null; 36 | 37 | return { 38 | customer, 39 | subscription, 40 | }; 41 | } catch (error) { 42 | console.error(error); 43 | return { 44 | error, 45 | }; 46 | } 47 | } 48 | 49 | async function getCustomer(data, context, stripe) { 50 | try { 51 | const customer = await stripe.customers.retrieve(data.customerId); 52 | console.log("retrieved customer", customer); 53 | return customer; 54 | } catch (error) { 55 | console.error("error retrieving customer", err); 56 | return { 57 | error, 58 | }; 59 | } 60 | } 61 | 62 | export default functions.https.onCall(async (data, context) => { 63 | console.log("data", data); 64 | 65 | const stripeSecretKey = await getSecret("stripe-secret-key"); 66 | const stripe = _stripe(stripeSecretKey); 67 | 68 | if (data.action === "create") { 69 | return createCustomer(data, context, stripe); 70 | } else if (data.action === "get") { 71 | return getCustomer(data, context, stripe); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /project/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@staqjs/client", 3 | "version": "0.0.32", 4 | "description": "StaqJS is a Javascript library for creating Software-as-a-Service (SaaS) businesses", 5 | "main": "dist/index.js", 6 | "source": "src/index.js", 7 | "engines": { 8 | "node": ">=10" 9 | }, 10 | "scripts": { 11 | "build": "NODE_ENV=production rollup -c", 12 | "start": "rollup --watch -c", 13 | "prepare": "run-s build", 14 | "test": "run-s test:unit test:lint test:build", 15 | "test:build": "run-s build", 16 | "test:lint": "eslint ." 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "dependencies": { 22 | "@stripe/react-stripe-js": "^1.1.2", 23 | "@stripe/stripe-js": "^1.8.0", 24 | "react-icons": "^4.2.0", 25 | "react-portal": "^4.2.1", 26 | "react-tooltip-controller": "^1.0.6" 27 | }, 28 | "peerDependencies": { 29 | "firebase": "^7.16.1", 30 | "react": "^16.13.1", 31 | "react-router": "^5.2.0", 32 | "react-router-dom": "^5.2.0" 33 | }, 34 | "devDependencies": { 35 | "autoprefixer": "^10.2.5", 36 | "babel-eslint": "^10.1.0", 37 | "cross-env": "^7.0.2", 38 | "eslint": "^7.5.0", 39 | "eslint-config-airbnb": "^18.2.0", 40 | "eslint-plugin-import": "^2.22.0", 41 | "eslint-plugin-jsx-a11y": "^6.3.1", 42 | "eslint-plugin-react": "^7.20.3", 43 | "eslint-plugin-react-hooks": "^4.0.8", 44 | "npm-run-all": "^4.1.5", 45 | "postcss": "^8.2.7", 46 | "prettier": "^2.0.4", 47 | "react-scripts": "^3.4.1", 48 | "rollup": "^2.40.0", 49 | "rollup-plugin-babel": "^4.4.0", 50 | "rollup-plugin-commonjs": "^10.1.0", 51 | "rollup-plugin-node-resolve": "^5.2.0", 52 | "rollup-plugin-postcss": "^4.0.0", 53 | "rollup-plugin-tailwindcss": "^1.0.0", 54 | "tailwindcss": "^2.0.3" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/staqjs/staq.git" 59 | }, 60 | "author": "Matt Roll", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/staqjs/staq/issues" 64 | }, 65 | "homepage": "https://github.com/staqjs/staq#readme" 66 | } 67 | -------------------------------------------------------------------------------- /project/client/src/lib/Auth/context.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { withFirebase } from '../Firebase' 4 | 5 | const AuthContext = React.createContext(null) 6 | 7 | export const withAuth = (Component) => (props) => ( 8 | 9 | {(auth) => } 10 | 11 | ) 12 | 13 | class AuthProvider extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | 17 | this.state = { 18 | currentUser: JSON.parse(localStorage.getItem('currentUser')), 19 | onLogoutCallback: () => { 20 | console.log('logging out') 21 | }, 22 | } 23 | } 24 | 25 | componentDidMount() { 26 | this.listener = this.props.firebase.onAuthUserListener( 27 | (currentUser) => { 28 | localStorage.setItem('currentUser', JSON.stringify(currentUser)) 29 | this.setState({ currentUser }) 30 | }, 31 | () => { 32 | localStorage.removeItem('currentUser') 33 | this.setState({ currentUser: null }) 34 | this.state.onLogoutCallback() 35 | }, 36 | ) 37 | } 38 | 39 | componentWillUnmount() { 40 | this.listener() 41 | } 42 | 43 | reload = () => { 44 | this.setState({ 45 | currentUser: JSON.parse(localStorage.getItem('currentUser')), 46 | }) 47 | } 48 | 49 | update = (newFields) => { 50 | const { firebase } = this.props 51 | 52 | firebase 53 | .user(this.state.currentUser.uid) 54 | .update(newFields) 55 | .then(() => { 56 | const currentUser = { 57 | ...this.state.currentUser, 58 | ...newFields, 59 | } 60 | localStorage.setItem('currentUser', JSON.stringify(currentUser)) 61 | this.setState({ currentUser }) 62 | }) 63 | } 64 | 65 | onLogout = (newCallbackOnLogout) => { 66 | this.setState({ 67 | onLogoutCallback: newCallbackOnLogout, 68 | }) 69 | } 70 | 71 | render() { 72 | const { children } = this.props 73 | const { currentUser } = this.state 74 | 75 | return ( 76 | 84 | {children} 85 | 86 | ) 87 | } 88 | } 89 | 90 | export default withFirebase(AuthProvider) 91 | -------------------------------------------------------------------------------- /project/client/src/components/Footer/FooterOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Portal } from 'react-portal' 4 | import _ from 'lodash' 5 | 6 | function FooterColumn(props) { 7 | const { Title, Links } = props 8 | 9 | return ( 10 |
11 |
12 | {Title} 13 |
14 |
15 | {Links.map((link) => { 16 | return _.startsWith(link, '/') ? ( 17 | 22 | {link.Text} 23 | 24 | ) : ( 25 | 30 | {' '} 31 | {link.Text}{' '} 32 | 33 | ) 34 | })} 35 |
36 |
37 | ) 38 | } 39 | 40 | function PoweredByStaqLabel() { 41 | return ( 42 | 51 | ) 52 | } 53 | 54 | function FooterOne(props) { 55 | const { Columns, Copyright, PoweredByStaq } = props 56 | 57 | return ( 58 | 59 |
64 |
65 | {Columns.map((column) => ( 66 | 71 | ))} 72 |
73 | 74 |
75 |
76 | 77 | © {Copyright} 78 | 79 |
80 | 81 | {PoweredByStaq || false ? : null} 82 |
83 |
84 |
85 | ) 86 | } 87 | 88 | export default FooterOne 89 | -------------------------------------------------------------------------------- /project/client/src/pages/ForgotPasswordPage/ForgotPasswordPageOne.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, Redirect } from 'react-router-dom' 3 | 4 | import { withFirebase } from '../../lib/Firebase' 5 | import { withAuth } from '../../lib/Auth' 6 | import staqConfig from '../../../../staq' 7 | 8 | function ForgotPasswordPageBase(props) { 9 | const { auth, firebase } = props 10 | const Logo = staqConfig.get('logo') || null 11 | 12 | return auth.currentUser ? ( 13 | 14 | ) : ( 15 |
16 | {Logo ? ( 17 | 18 | 19 | 20 | ) : null} 21 | 22 | 23 |
24 | ) 25 | } 26 | 27 | function ForgotPasswordForm(props) { 28 | const { firebase } = props 29 | 30 | const [email, setEmail] = React.useState('') 31 | const [error, setError] = React.useState('') 32 | 33 | const onSubmit = (event) => { 34 | firebase.auth 35 | .sendPasswordResetEmail(email) 36 | .then(() => { 37 | setError(`Sent reset instructions to ${email}`) 38 | }) 39 | .catch(() => { 40 | setError('Error sending reset email') 41 | }) 42 | event.preventDefault() 43 | } 44 | 45 | const onChangeEmail = (event) => { 46 | setEmail(event.target.value) 47 | } 48 | 49 | return ( 50 |
53 | Reset Password 54 | 55 |
56 | 64 | 72 | 73 | {error && {error}} 74 |
75 |
76 | ) 77 | } 78 | 79 | function ForgotPasswordPage(props) { 80 | return 81 | } 82 | 83 | export default withFirebase(withAuth(ForgotPasswordPage)) 84 | -------------------------------------------------------------------------------- /project/client/src/lib/signup.js: -------------------------------------------------------------------------------- 1 | import staqConfig from '../../../staq' 2 | 3 | const noop = () => {} 4 | 5 | async function createStripeCustomer(email, onSuccess, onError) { 6 | const firebase = staqConfig.get('firebase') 7 | const stripeCustomerFn = firebase.functions.httpsCallable('stripeCustomer') 8 | stripeCustomerFn({ 9 | action: 'create', 10 | customer: { email }, 11 | }) 12 | .then((result) => { 13 | onSuccess(result) 14 | }) 15 | .catch(onError) 16 | } 17 | 18 | const getStripeAttributes = (stripeData) => { 19 | const usePayments = staqConfig.get('Payments.Enabled') 20 | const paymentType = staqConfig.get('Payments.Type') 21 | const stripeAttributes = {} 22 | 23 | if (usePayments) { 24 | stripeAttributes.stripeCustomerId = stripeData.customer.id 25 | 26 | if (paymentType === 'subscription') { 27 | stripeAttributes.subscriptionPriceId = 28 | stripeData.subscription.items.data[0].price.id 29 | } 30 | } 31 | 32 | return stripeAttributes 33 | } 34 | 35 | const createFirebaseUser = ( 36 | email, 37 | password, 38 | stripeData, 39 | onSuccess, 40 | onError, 41 | ) => { 42 | const firebase = staqConfig.get('firebase') 43 | const stripeAttributes = getStripeAttributes(stripeData) 44 | const userDefaults = staqConfig.get('UserDefaults', {}) 45 | 46 | firebase 47 | .doCreateUserWithEmailAndPassword(email, password) 48 | .then((currentUser) => { 49 | const userData = { 50 | email, 51 | uid: currentUser.user.uid, 52 | ...stripeAttributes, 53 | ...userDefaults, 54 | } 55 | return firebase 56 | .user(currentUser.user.uid) 57 | .set(userData) 58 | .then(() => { 59 | onSuccess(userData) 60 | }) 61 | }) 62 | .catch(onError) 63 | } 64 | 65 | const submitWithPaymentsEnabled = (userInfo, onSuccess, onError) => { 66 | createStripeCustomer(userInfo.email, (response) => { 67 | if (response.statusCode && response.statusCode !== 200) { 68 | onError(response.code) 69 | } else { 70 | createFirebaseUser( 71 | userInfo.email, 72 | userInfo.password, 73 | response.data, 74 | onSuccess, 75 | ) 76 | } 77 | }) 78 | } 79 | 80 | const submitWithPaymentsDisabled = ( 81 | userInfo, 82 | onSuccess = noop, 83 | onError = noop, 84 | ) => { 85 | createFirebaseUser( 86 | userInfo.email, 87 | userInfo.password, 88 | null, 89 | onSuccess, 90 | onError, 91 | ) 92 | } 93 | 94 | export const signup = (userInfo, onSuccess = noop, onError = noop) => { 95 | const submitFn = staqConfig.get('Payments.Enabled') 96 | ? submitWithPaymentsEnabled 97 | : submitWithPaymentsDisabled 98 | 99 | submitFn(userInfo, onSuccess, onError) 100 | } 101 | -------------------------------------------------------------------------------- /project/client/src/lib/Firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import app from 'firebase/app' 2 | import 'firebase/analytics' 3 | import 'firebase/auth' 4 | import 'firebase/firestore' 5 | import 'firebase/functions' 6 | 7 | import staqConfig from '../../../../staq' 8 | 9 | class Firebase { 10 | constructor(config) { 11 | app.initializeApp(config) 12 | 13 | this.auth = app.auth() 14 | this.db = app.firestore() 15 | this.analytics = app.analytics() 16 | this.functions = app.functions() 17 | 18 | if (window.location.host.includes('localhost')) { 19 | this.functions.useFunctionsEmulator('http://localhost:5001') 20 | } 21 | } 22 | 23 | logEvent = (eventName, eventParams) => { 24 | this.analytics.logEvent(eventName, eventParams) 25 | } 26 | 27 | // *** Auth API *** 28 | doCreateUserWithEmailAndPassword = (email, password) => 29 | this.auth.createUserWithEmailAndPassword(email, password) 30 | 31 | doSignInWithEmailAndPassword = (email, password) => 32 | this.auth.signInWithEmailAndPassword(email, password) 33 | 34 | doSignOut = () => this.auth.signOut() 35 | 36 | doPasswordReset = (email) => this.auth.sendPasswordResetEmail(email) 37 | 38 | doPasswordUpdate = (password) => 39 | this.auth.currentUser.updatePassword(password) 40 | 41 | doSendEmailVerification = () => 42 | this.auth.currentUser.sendEmailVerification({ 43 | url: staqConfig.get('BaseUrl') 44 | }) 45 | 46 | // *** Merge Auth and DB User API *** // 47 | 48 | onAuthUserListener = (next, fallback) => 49 | this.auth.onAuthStateChanged((currentUser) => { 50 | if (currentUser) { 51 | this.user(currentUser.uid) 52 | .get() 53 | .then((snapshot) => { 54 | const dbUser = snapshot.data() 55 | 56 | // merge auth and db user 57 | const user = { 58 | uid: currentUser.uid, 59 | email: currentUser.email, 60 | emailVerified: currentUser.emailVerified, 61 | providerData: currentUser.providerData, 62 | ...dbUser 63 | } 64 | 65 | next(user) 66 | }) 67 | } else { 68 | fallback() 69 | } 70 | }) 71 | 72 | // *** User API *** 73 | 74 | user = (uid) => this.db.collection('users').doc(uid) 75 | 76 | users = () => this.db.collection('users') 77 | 78 | collection = (collectionName) => this.db.collection(collectionName) 79 | 80 | collectionForUser = (collectionName, uid) => 81 | this.collection(collectionName).where('uid', '==', uid) 82 | 83 | document = (collectionName, documentId) => 84 | this.collection(collectionName).doc(documentId) 85 | 86 | addDocumentForUser = (uid, collectionName, document) => 87 | this.db.collection(collectionName).add({ 88 | uid, 89 | ...document 90 | }) 91 | 92 | updateDocument = (collectionName, documentId, updateFields) => { 93 | return this.db 94 | .collection(collectionName) 95 | .doc(documentId) 96 | .update(updateFields) 97 | } 98 | } 99 | 100 | export default Firebase 101 | -------------------------------------------------------------------------------- /project/client/src/components/SignInForm/SignInFormOne.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Link, useHistory } from 'react-router-dom' 3 | 4 | import { withFirebase } from '../../lib/Firebase' 5 | import * as Routes from '../../constants/routes' 6 | 7 | function SignInFormOne(props) { 8 | const history = useHistory() 9 | const [state, setState] = useState({ 10 | email: '', 11 | password: '', 12 | }) 13 | const { firebase } = props 14 | const [loading, setLoading] = useState(false) 15 | const [error, setError] = useState(null) 16 | 17 | const setField = (field, value) => { 18 | setState({ 19 | ...state, 20 | [field]: value, 21 | }) 22 | } 23 | 24 | const onSubmit = (e) => { 25 | firebase 26 | .doSignInWithEmailAndPassword(state.email, state.password) 27 | .then(() => { 28 | setState({ 29 | email: '', 30 | password: '', 31 | }) 32 | }) 33 | .catch((error) => { 34 | setError('Please enter a valid username and password.') 35 | }) 36 | 37 | e.preventDefault() 38 | } 39 | 40 | return ( 41 |
46 |
Sign In
47 | 48 |
49 |
50 |
51 | setField('email', event.target.value)} 54 | placeholder="Email" 55 | className={ 56 | 'focus:sjs-border-light-blue-500 focus:sjs-ring-1 focus:sjs-ring-light-blue-500 focus:sjs-outline-none sjs-w-full sjs-text-sm sjs-text-black sjs-placeholder-gray-500 sjs-border sjs-border-gray-200 sjs-rounded-md sjs-py-2 sjs-pl-2 sjs-mb-2' 57 | } 58 | /> 59 | 60 | setField('password', event.target.value)} 64 | placeholder="Password" 65 | className={ 66 | 'focus:sjs-border-light-blue-500 focus:sjs-ring-1 focus:sjs-ring-light-blue-500 focus:sjs-outline-none sjs-w-full sjs-text-sm sjs-text-black sjs-placeholder-gray-500 sjs-border sjs-border-gray-200 sjs-rounded-md sjs-py-2 sjs-pl-2 sjs-mb-2' 67 | } 68 | /> 69 |
70 | 71 | 79 | 80 | 81 | Forgot your password? 82 | 83 | 84 | {error && ( 85 |
86 | {error} 87 |
88 | )} 89 |
90 |
91 |
92 | ) 93 | } 94 | 95 | export default withFirebase(SignInFormOne) 96 | -------------------------------------------------------------------------------- /project/client/src/lib/StaqRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Redirect, Route, useLocation, useHistory } from 'react-router-dom' 3 | import _ from 'lodash' 4 | 5 | import staqConfig from '../../../staq' 6 | 7 | import { withAuth } from './Auth' 8 | 9 | import LandingPage from '../pages/LandingPage/LandingPage' 10 | import SignUpPage from '../pages/SignUpPage/SignUpPage' 11 | import SignInPage from '../pages/SignInPage/SignInPage' 12 | import PricingPage from '../pages/PricingPage/PricingPage' 13 | import ForgotPasswordPage from '../pages/ForgotPasswordPage/ForgotPasswordPage' 14 | 15 | import * as Footers from '../components/Footer' 16 | import * as Navbars from '../components/Navigation' 17 | import * as Routes from '../constants/routes' 18 | 19 | function ScrollToTop() { 20 | const { pathname } = useLocation() 21 | 22 | useEffect(() => { 23 | window.scrollTo(0, 0) 24 | }, [pathname]) 25 | 26 | return null 27 | } 28 | 29 | function PrivateRouteBase({ component: Component, auth, ...rest }) { 30 | return ( 31 | { 34 | return auth.currentUser ? ( 35 | 36 | ) : ( 37 | 38 | ) 39 | }} 40 | /> 41 | ) 42 | } 43 | 44 | const footerRoutes = [ 45 | '/', 46 | '/demo', 47 | '/signin', 48 | '/signup', 49 | '/pricing', 50 | '/forgot-password', 51 | ] 52 | 53 | function StaqRoutes() { 54 | const history = useHistory() 55 | const [pathname, setPathname] = useState(history.location.pathname) 56 | 57 | history.listen((location) => { 58 | setPathname(location.pathname) 59 | }) 60 | 61 | const template = staqConfig.get('Template.Name') 62 | 63 | const navbarRoutes = staqConfig.get('navbarRoutes') 64 | const Navbar = Navbars[`Navbar${template}`] 65 | 66 | const footerColumns = staqConfig.get('Template.Config.Footer.Columns', {}) 67 | const copyright = staqConfig.get('Template.Config.Footer.Copyright') 68 | const poweredByStaq = staqConfig.get( 69 | 'Template.Config.Footer.PoweredByStaq', 70 | false, 71 | ) 72 | const Footer = Footers[`Footer${template}`] 73 | 74 | return ( 75 | <> 76 | 77 | {_.isNil(navbarRoutes) ? ( 78 | 79 | ) : navbarRoutes.includes(pathname) ? ( 80 | 81 | ) : null} 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {footerRoutes.includes(pathname) ? ( 91 |