From 714462435da1bb6173082f2871654e2a71f140c0 Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Fri, 12 Apr 2024 15:48:25 +0530 Subject: [PATCH] Reuse pages from captn (#11) * Add toc and privacy page * Update auth component * Update customer portal link * Update CI action file --- .github/workflows/pipeline.yml | 135 +++---- app/main.wasp | 14 +- .../migration.sql | 4 + app/src/client/App.tsx | 92 ++++- app/src/client/Main.css | 197 +++++++++ app/src/client/app/AccountPage.tsx | 11 +- app/src/client/app/PrivacyPage.tsx | 377 ++++++++++++++++++ app/src/client/app/TocPage.tsx | 197 +++++++++ app/src/client/auth/Auth.tsx | 110 +++++ app/src/client/auth/LoginPage.tsx | 31 +- app/src/client/auth/LoginSignupForm.tsx | 181 +++++++++ app/src/client/auth/SignupPage.tsx | 14 +- app/src/client/auth/configs/config.ts | 11 + .../client/auth/configs/stitches.config.js | 30 ++ .../components/AnimatedCharacterLoader.tsx | 2 +- .../MarketingEmailPreferenceSwitcher.tsx | 80 ++++ app/src/client/components/NotificationBox.tsx | 32 ++ .../components/TosAndMarketingEmails.tsx | 72 ++++ .../components/TosAndMarketingEmailsModal.tsx | 93 +++++ app/src/server/scripts/usersSeed.ts | 3 + app/src/shared/constants.ts | 2 +- 21 files changed, 1583 insertions(+), 105 deletions(-) create mode 100644 app/migrations/20240412085104_add_fields_to_user_entity_to_trach_signup_complete_status/migration.sql create mode 100644 app/src/client/app/PrivacyPage.tsx create mode 100644 app/src/client/app/TocPage.tsx create mode 100644 app/src/client/auth/Auth.tsx create mode 100644 app/src/client/auth/LoginSignupForm.tsx create mode 100644 app/src/client/auth/configs/config.ts create mode 100644 app/src/client/auth/configs/stitches.config.js create mode 100644 app/src/client/components/MarketingEmailPreferenceSwitcher.tsx create mode 100644 app/src/client/components/NotificationBox.tsx create mode 100644 app/src/client/components/TosAndMarketingEmails.tsx create mode 100644 app/src/client/components/TosAndMarketingEmailsModal.tsx diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index b647118..e5befc0 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -81,53 +81,53 @@ jobs: run: docker push ghcr.io/$GITHUB_REPOSITORY --all-tags - # deploy_backend: - # runs-on: ubuntu-22.04 - # defaults: - # run: - # shell: bash - # if: github.ref_name == 'main' || github.ref_name == 'dev' - # env: - # GITHUB_USERNAME: ${{ github.actor }} - # GITHUB_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - # PORT: ${{ vars.PORT }} - # REACT_APP_CUSTOMER_PORTAL_LINK: ${{ vars.REACT_APP_CUSTOMER_PORTAL_LINK }} - # GOOGLE_CLIENT_ID: ${{ github.ref_name == 'main' && secrets.PROD_GOOGLE_CLIENT_ID || secrets.STAGING_GOOGLE_CLIENT_ID }} - # GOOGLE_CLIENT_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_GOOGLE_CLIENT_SECRET || secrets.STAGING_GOOGLE_CLIENT_SECRET }} - # ADMIN_EMAILS: ${{ vars.ADMIN_EMAILS }} - - # WASP_SERVER_URL: ${{ github.ref_name == 'main' && vars.PROD_WASP_SERVER_URL || vars.STAGING_WASP_SERVER_URL }} - # ADS_SERVER_URL: ${{ github.ref_name == 'main' && vars.PROD_ADS_SERVER_URL || vars.STAGING_ADS_SERVER_URL }} - # BACKEND_DOMAIN: ${{ github.ref_name == 'main' && vars.PROD_BACKEND_DOMAIN || vars.STAGING_BACKEND_DOMAIN }} - # WASP_WEB_CLIENT_URL: ${{ github.ref_name == 'main' && vars.PROD_WASP_WEB_CLIENT_URL || vars.STAGING_WASP_WEB_CLIENT_URL }} - # DATABASE_URL: ${{ github.ref_name == 'main' && secrets.PROD_DATABASE_URL || secrets.STAGING_DATABASE_URL }} - # REACT_APP_API_URL: ${{ github.ref_name == 'main' && vars.PROD_REACT_APP_API_URL || vars.STAGING_REACT_APP_API_URL }} - # JWT_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_JWT_SECRET || secrets.STAGING_JWT_SECRET }} - # PRO_SUBSCRIPTION_PRICE_ID: ${{ github.ref_name == 'main' && secrets.PROD_PRO_SUBSCRIPTION_PRICE_ID || secrets.STAGING_PRO_SUBSCRIPTION_PRICE_ID }} - # STRIPE_KEY: ${{ github.ref_name == 'main' && secrets.PROD_STRIPE_KEY || secrets.STAGING_STRIPE_KEY }} - # STRIPE_WEBHOOK_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_STRIPE_WEBHOOK_SECRET || secrets.STAGING_STRIPE_WEBHOOK_SECRET }} - # SSH_KEY: ${{ github.ref_name == 'main' && secrets.PROD_SSH_KEY || secrets.STAGING_SSH_KEY }} - # steps: - # - uses: actions/checkout@v3 - # # This is to fix GIT not liking owner of the checkout dir - https://github.com/actions/runner/issues/2033#issuecomment-1204205989 - # - run: chown -R $(id -u):$(id -g) $PWD - - # - run: if [[ $GITHUB_REF_NAME == "main" ]]; then echo "TAG=latest" >> $GITHUB_ENV ; else echo "TAG=dev" >> $GITHUB_ENV ; fi; - - # - run: echo "PATH=$PATH:/github/home/.local/bin" >> $GITHUB_ENV - # - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )' - # - run: eval $(ssh-agent -s) - # - run: mkdir -p ~/.ssh - # - run: chmod 700 ~/.ssh - # - run: ssh-keyscan "$BACKEND_DOMAIN" >> ~/.ssh/known_hosts - # - run: chmod 644 ~/.ssh/known_hosts - # - run: echo "$SSH_KEY" | base64 --decode > key.pem - # - run: chmod 600 key.pem - - # - run: ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$BACKEND_DOMAIN" "docker images" - # - run: bash scripts/deploy_backend.sh - - # - run: rm key.pem + deploy_backend: + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + if: github.ref_name == 'main' || github.ref_name == 'dev' + env: + GITHUB_USERNAME: ${{ github.actor }} + GITHUB_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + PORT: ${{ vars.PORT }} + REACT_APP_CUSTOMER_PORTAL_LINK: ${{ vars.REACT_APP_CUSTOMER_PORTAL_LINK }} + GOOGLE_CLIENT_ID: ${{ github.ref_name == 'main' && secrets.PROD_GOOGLE_CLIENT_ID || secrets.STAGING_GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_GOOGLE_CLIENT_SECRET || secrets.STAGING_GOOGLE_CLIENT_SECRET }} + ADMIN_EMAILS: ${{ vars.ADMIN_EMAILS }} + + WASP_SERVER_URL: ${{ github.ref_name == 'main' && vars.PROD_WASP_SERVER_URL || vars.STAGING_WASP_SERVER_URL }} + ADS_SERVER_URL: ${{ github.ref_name == 'main' && vars.PROD_ADS_SERVER_URL || vars.STAGING_ADS_SERVER_URL }} + BACKEND_DOMAIN: ${{ github.ref_name == 'main' && vars.PROD_BACKEND_DOMAIN || vars.STAGING_BACKEND_DOMAIN }} + WASP_WEB_CLIENT_URL: ${{ github.ref_name == 'main' && vars.PROD_WASP_WEB_CLIENT_URL || vars.STAGING_WASP_WEB_CLIENT_URL }} + DATABASE_URL: ${{ github.ref_name == 'main' && secrets.PROD_DATABASE_URL || secrets.STAGING_DATABASE_URL }} + REACT_APP_API_URL: ${{ github.ref_name == 'main' && vars.PROD_REACT_APP_API_URL || vars.STAGING_REACT_APP_API_URL }} + JWT_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_JWT_SECRET || secrets.STAGING_JWT_SECRET }} + PRO_SUBSCRIPTION_PRICE_ID: ${{ github.ref_name == 'main' && secrets.PROD_PRO_SUBSCRIPTION_PRICE_ID || secrets.STAGING_PRO_SUBSCRIPTION_PRICE_ID }} + STRIPE_KEY: ${{ github.ref_name == 'main' && secrets.PROD_STRIPE_KEY || secrets.STAGING_STRIPE_KEY }} + STRIPE_WEBHOOK_SECRET: ${{ github.ref_name == 'main' && secrets.PROD_STRIPE_WEBHOOK_SECRET || secrets.STAGING_STRIPE_WEBHOOK_SECRET }} + SSH_KEY: ${{ github.ref_name == 'main' && secrets.PROD_SSH_KEY || secrets.STAGING_SSH_KEY }} + steps: + - uses: actions/checkout@v3 + # This is to fix GIT not liking owner of the checkout dir - https://github.com/actions/runner/issues/2033#issuecomment-1204205989 + - run: chown -R $(id -u):$(id -g) $PWD + + - run: if [[ $GITHUB_REF_NAME == "main" ]]; then echo "TAG=latest" >> $GITHUB_ENV ; else echo "TAG=dev" >> $GITHUB_ENV ; fi; + + - run: echo "PATH=$PATH:/github/home/.local/bin" >> $GITHUB_ENV + - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )' + - run: eval $(ssh-agent -s) + - run: mkdir -p ~/.ssh + - run: chmod 700 ~/.ssh + - run: ssh-keyscan "$BACKEND_DOMAIN" >> ~/.ssh/known_hosts + - run: chmod 644 ~/.ssh/known_hosts + - run: echo "$SSH_KEY" | base64 --decode > key.pem + - run: chmod 600 key.pem + + - run: ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$BACKEND_DOMAIN" "docker images" + - run: bash scripts/deploy_backend.sh + + - run: rm key.pem deploy_frontend: runs-on: ubuntu-22.04 @@ -135,13 +135,10 @@ jobs: contents: write if: github.ref_name == 'main' || github.ref_name == 'dev' env: - # STAGING_BACKEND_DOMAIN: ${{ vars.STAGING_BACKEND_DOMAIN }} - # STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} - # REACT_APP_CUSTOMER_PORTAL_LINK: ${{ vars.REACT_APP_CUSTOMER_PORTAL_LINK }} - # REACT_APP_API_URL: ${{ github.ref_name == 'main' && vars.PROD_REACT_APP_API_URL || vars.STAGING_REACT_APP_API_URL }} - - REACT_APP_CUSTOMER_PORTAL_LINK: "" - REACT_APP_API_URL: "" + STAGING_BACKEND_DOMAIN: ${{ vars.STAGING_BACKEND_DOMAIN }} + STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} + REACT_APP_CUSTOMER_PORTAL_LINK: ${{ vars.REACT_APP_CUSTOMER_PORTAL_LINK }} + REACT_APP_API_URL: ${{ github.ref_name == 'main' && vars.PROD_REACT_APP_API_URL || vars.STAGING_REACT_APP_API_URL }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -171,18 +168,18 @@ jobs: with: folder: app/.wasp/build/web-app/build - # - name: Deploy UI to staging - # if: github.ref_name == 'dev' - # run: | - # apt-get update -y && apt-get install openssh-client git -y - # eval $(ssh-agent -s) - # mkdir -p ~/.ssh - # chmod 700 ~/.ssh - # ssh-keyscan "$STAGING_BACKEND_DOMAIN" >> ~/.ssh/known_hosts - # chmod 644 ~/.ssh/known_hosts - # echo "$STAGING_SSH_KEY" | base64 --decode > key.pem - # chmod 600 key.pem - # ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$STAGING_BACKEND_DOMAIN" "ls -lah /var/www/html/UI" - # scp -i key.pem -r app/.wasp/build/web-app/build azureuser@"$STAGING_BACKEND_DOMAIN":/var/www/html/UI - # ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$STAGING_BACKEND_DOMAIN" "ls -lah /var/www/html/UI" - # rm key.pem + - name: Deploy UI to staging + if: github.ref_name == 'dev' + run: | + apt-get update -y && apt-get install openssh-client git -y + eval $(ssh-agent -s) + mkdir -p ~/.ssh + chmod 700 ~/.ssh + ssh-keyscan "$STAGING_BACKEND_DOMAIN" >> ~/.ssh/known_hosts + chmod 644 ~/.ssh/known_hosts + echo "$STAGING_SSH_KEY" | base64 --decode > key.pem + chmod 600 key.pem + ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$STAGING_BACKEND_DOMAIN" "ls -lah /var/www/html/UI" + scp -i key.pem -r app/.wasp/build/web-app/build azureuser@"$STAGING_BACKEND_DOMAIN":/var/www/html/UI + ssh -o StrictHostKeyChecking=no -i key.pem azureuser@"$STAGING_BACKEND_DOMAIN" "ls -lah /var/www/html/UI" + rm key.pem diff --git a/app/main.wasp b/app/main.wasp index 69e25d1..1c55f03 100644 --- a/app/main.wasp +++ b/app/main.wasp @@ -31,7 +31,7 @@ app OpenSaaS { }, }, onAuthFailedRedirectTo: "/login", - onAuthSucceededRedirectTo: "/", + onAuthSucceededRedirectTo: "/chat", }, db: { system: PostgreSQL, @@ -67,6 +67,9 @@ entity User {=psl createdAt DateTime @default(now()) lastActiveTimestamp DateTime @default(now()) isAdmin Boolean @default(false) + hasAcceptedTos Boolean @default(false) + hasSubscribedToMarketingEmails Boolean @default(false) + isSignUpComplete Boolean @default(false) stripeId String? checkoutSessionId String? hasPaid Boolean @default(false) @@ -148,6 +151,15 @@ page CheckoutPage { component: import Checkout from "@src/client/app/CheckoutPage" } +route TocPageRoute { path: "/toc", to: TocPage } +page TocPage { + component: import TocPage from "@src/client/app/TocPage", +} +route PrivacyRoute { path: "/privacy", to: PrivacyPage } +page PrivacyPage { + component: import PrivacyPage from "@src/client/app/PrivacyPage", +} + route AdminRoute { path: "/admin", to: DashboardPage } page DashboardPage { authRequired: true, diff --git a/app/migrations/20240412085104_add_fields_to_user_entity_to_trach_signup_complete_status/migration.sql b/app/migrations/20240412085104_add_fields_to_user_entity_to_trach_signup_complete_status/migration.sql new file mode 100644 index 0000000..29da96f --- /dev/null +++ b/app/migrations/20240412085104_add_fields_to_user_entity_to_trach_signup_complete_status/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "hasAcceptedTos" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "hasSubscribedToMarketingEmails" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isSignUpComplete" BOOLEAN NOT NULL DEFAULT false; diff --git a/app/src/client/App.tsx b/app/src/client/App.tsx index 7246ffa..546e916 100644 --- a/app/src/client/App.tsx +++ b/app/src/client/App.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect, ReactNode } from 'react'; +import { useMemo, useEffect, ReactNode, useState } from 'react'; import { useLocation } from 'react-router-dom'; import './Main.css'; @@ -10,6 +10,7 @@ import AppNavBar from './components/AppNavBar'; import Footer from './components/Footer'; import ServerNotRechableComponent from './components/ServerNotRechableComponent'; import LoadingComponent from './components/LoadingComponent'; +import TosAndMarketingEmailsModal from './components/TosAndMarketingEmailsModal'; const addServerErrorClass = () => { if (!document.body.classList.contains('server-error')) { @@ -29,6 +30,7 @@ const removeServerErrorClass = () => { */ export default function App({ children }: { children: ReactNode }) { const location = useLocation(); + const [showTosAndMarketingEmailsModal, setShowTosAndMarketingEmailsModal] = useState(false); const { data: user, isError, isLoading } = useAuth(); const shouldDisplayAppNavBar = useMemo(() => { @@ -39,12 +41,48 @@ export default function App({ children }: { children: ReactNode }) { return location.pathname.startsWith('/admin'); }, [location]); + const isCheckoutPage = useMemo(() => { + return location.pathname.startsWith('/checkout'); + }, [location]); + + const isAccountPage = useMemo(() => { + return location.pathname.startsWith('/account'); + }, [location]); + + const isChatPage = useMemo(() => { + return location.pathname.startsWith('/chat'); + }, [location]); + useEffect(() => { if (user) { - const lastSeenAt = new Date(user.lastActiveTimestamp); - const today = new Date(); - if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) { - updateCurrentUser({ lastActiveTimestamp: today }); + console.log('user', user); + if (!user.isSignUpComplete) { + if (user.hasAcceptedTos) { + updateCurrentUser({ + isSignUpComplete: true, + }); + setShowTosAndMarketingEmailsModal(false); + } else { + const hasAcceptedTos = localStorage.getItem('hasAcceptedTos') === 'true'; + const hasSubscribedToMarketingEmails = localStorage.getItem('hasSubscribedToMarketingEmails') === 'true'; + if (!hasAcceptedTos) { + setShowTosAndMarketingEmailsModal(true); + } else { + updateCurrentUser({ + isSignUpComplete: true, + hasAcceptedTos: hasAcceptedTos, + hasSubscribedToMarketingEmails: hasSubscribedToMarketingEmails, + }); + setShowTosAndMarketingEmailsModal(false); + } + } + } else { + setShowTosAndMarketingEmailsModal(false); + const lastSeenAt = new Date(user.lastActiveTimestamp); + const today = new Date(); + if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) { + updateCurrentUser({ lastActiveTimestamp: today }); + } } } }, [user]); @@ -63,13 +101,51 @@ export default function App({ children }: { children: ReactNode }) { <>
{isError && (addServerErrorClass(), ())} - {isAdminDashboard ? ( - <>{children} + {isAdminDashboard || isChatPage ? ( + <> + {showTosAndMarketingEmailsModal ? ( + <> + + + ) : ( + <> + {isAdminDashboard ? ( + children + ) : ( +
+ {shouldDisplayAppNavBar && } + {children} +
+
+
+

+ © 2024 airt. All rights reserved. +

+
+
+
+ )} + + )} + ) : (
{shouldDisplayAppNavBar && }
- {isError ? children : isLoading ? : (removeServerErrorClass(), children)} + {isError ? ( + children + ) : isLoading ? ( + + ) : ( + (removeServerErrorClass(), + showTosAndMarketingEmailsModal && (isCheckoutPage || isAccountPage) ? ( + <> + + + ) : ( + children + )) + )}
diff --git a/app/src/client/Main.css b/app/src/client/Main.css index b41b4c5..21cbbff 100644 --- a/app/src/client/Main.css +++ b/app/src/client/Main.css @@ -27,6 +27,203 @@ code { monospace; } + +.toc-marketing-checkbox-wrapper .checkbox-container { + padding-left: 22px; + display: block; + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +/* Hide the browser's default checkbox */ +.toc-marketing-checkbox-wrapper input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} +/* Create a custom checkbox */ +.toc-marketing-checkbox-wrapper .checkmark { + position: absolute; + top: 2px; + left: 0; + height: 13px; + width: 13px; + background-color: #eae4d9; +} + +.toc-marketing-checkbox-wrapper.light .checkmark { + background-color: #fff; +} + +/* On mouse-over, add a grey background color */ +.toc-marketing-checkbox-wrapper .checkbox-container:hover input ~ .checkmark { + background-color: #eae4d9; +} +.toc-marketing-checkbox-wrapper.light .checkbox-container:hover input ~ .checkmark { + background-color: #fff; +} + + +/* When the checkbox is checked, add a blue background */ +.toc-marketing-checkbox-wrapper .checkbox-container input:checked ~ .checkmark { + background-color: #6faabc; +} +.toc-marketing-checkbox-wrapper.light .checkbox-container input:checked ~ .checkmark { + background-color: #fff; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.toc-marketing-checkbox-wrapper .checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.toc-marketing-checkbox-wrapper .checkbox-container input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.toc-marketing-checkbox-wrapper .checkbox-container .checkmark:after { + left: 4px; + top: 0px; + width: 6px; + height: 11px; + border: solid #eae4d9; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +.toc-marketing-checkbox-wrapper.light .checkbox-container .checkmark:after { + border: solid #6faabc; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.toc-marketing-container{ + -webkit-transition: max-height 0.5s; + -moz-transition: max-height 0.5s; + -ms-transition: max-height 0.5s; + -o-transition: max-height 0.5s; + transition: max-height 0.5s; + overflow: hidden; +} + +/* Google login button */ +.gsi-material-button { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -webkit-appearance: none; + background-color: WHITE; + background-image: none; + border: 1px solid #747775; + -webkit-border-radius: 4px; + border-radius: 4px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #1f1f1f; + cursor: pointer; + font-family: 'Roboto', arial, sans-serif; + font-size: 14px; + height: 40px; + letter-spacing: 0.25px; + outline: none; + overflow: hidden; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background-color .218s, border-color .218s, box-shadow .218s; + transition: background-color .218s, border-color .218s, box-shadow .218s; + vertical-align: middle; + white-space: nowrap; + width: auto; + max-width: 400px; + min-width: min-content; +} + +.gsi-material-button .gsi-material-button-icon { + height: 20px; + margin-right: 12px; + min-width: 20px; + width: 20px; +} + +.gsi-material-button .gsi-material-button-content-wrapper { + -webkit-align-items: center; + align-items: center; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + flex-wrap: nowrap; + height: 100%; + justify-content: center; + position: relative; + width: 100%; +} + +.gsi-material-button .gsi-material-button-contents { + -webkit-flex-grow: 0; + flex-grow: 0; + font-family: 'Roboto', arial, sans-serif; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +.gsi-material-button .gsi-material-button-state { + -webkit-transition: opacity .218s; + transition: opacity .218s; + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; +} + +.gsi-material-button:disabled { + cursor: default; + background-color: #ffffff61; + border-color: #1f1f1f1f; +} + +.gsi-material-button:disabled .gsi-material-button-contents { + opacity: 38%; +} + +.gsi-material-button:disabled .gsi-material-button-icon { + opacity: 38%; +} + +.gsi-material-button:not(:disabled):active .gsi-material-button-state, +.gsi-material-button:not(:disabled):focus .gsi-material-button-state { + background-color: #303030; + opacity: 12%; +} + +.gsi-material-button:not(:disabled):hover { + -webkit-box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); + box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); +} + +.gsi-material-button:not(:disabled):hover .gsi-material-button-state { + background-color: #303030; + opacity: 8%; +} +/* Google login button */ + @layer utilities { /* Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { diff --git a/app/src/client/app/AccountPage.tsx b/app/src/client/app/AccountPage.tsx index bec1673..7f23888 100644 --- a/app/src/client/app/AccountPage.tsx +++ b/app/src/client/app/AccountPage.tsx @@ -5,6 +5,7 @@ import { STRIPE_CUSTOMER_PORTAL_LINK } from '../../shared/constants'; import { TierIds } from '../../shared/constants'; import Button from '../components/Button'; import FreeTrialButton from '../components/FreeTrialButton'; +import { MarketingEmailPreferenceSwitcher } from '../components/MarketingEmailPreferenceSwitcher'; export default function AccountPage({ user }: { user: User }) { return ( @@ -63,10 +64,12 @@ export default function AccountPage({ user }: { user: User }) { )}
-
About
-
- I'm a cool customer. -
+
I agree to receiving marketing emails
+ <> + +
diff --git a/app/src/client/app/PrivacyPage.tsx b/app/src/client/app/PrivacyPage.tsx new file mode 100644 index 0000000..74fd347 --- /dev/null +++ b/app/src/client/app/PrivacyPage.tsx @@ -0,0 +1,377 @@ +export default function PrivacyPage() { + return ( +
+
+
+

Privacy Policy

+

Last updated January 29, 2024

+ +
+

+ Airt technologies, Inc. ("we", "us", or "our") is committed to protecting the privacy of our users. This + Privacy Policy explains how we collect, use, and disclose information through our SaaS tool,{' '} + Capt’n.ai (the "Service"). +

+
+ +
+

Information We Collect

+

+ At Capt’n.ai, we value your privacy and are committed to ensuring the highest level of confidentiality and + security for your information. Here's what you need to know about the information we collect when you use + our Service: +

+
+
    +
  • + Account Information: When you create a Capt’n.ai account, we collect your name and email address. + This information is essential to personalize your experience and enable various features of the Service. +
  • +
  • + Integrations: As you integrate your various marketing platforms with our Service, we collect + information that you input, including the details about the platforms you're connecting and any + associated data. This data is necessary to provide you with accurate analytics, reports, and insights. + For data collected from Google APIs, we adhere to Google's API Services User Data Policy, including the + Limited Use requirements. Continue reading for further information. +
  • +
  • + Chat Interactions and AI Data Sharing: Your chat interactions and data from connected third-party + services may be shared with our privately deployed OpenAI models hosted on Microsoft Azure. This sharing + is essential for the service and is detailed in the section below: "Third-Party and Proprietary AI + Tools." +
  • +
  • + Usage Information: To help us understand how you interact with our Service and enable us to + improve your user experience, we collect information about your usage. This may include log data, device + information, and other data related to your activities within our Service. +
  • +
  • + Data Processing: At Capt’n.ai, your data's privacy is a top priority. We process data on-the-fly + and do not store any data in databases, except for chat history as detailed in the "Third-Party and + Proprietary AI Tools" section. This ensures your data stays where it belongs—with you. +
  • +
+
+ +
+

We use the information we collect to:

+
+
    +
  • + Provide, Maintain, and Improve the Service: We use your information to deliver the services you + request, maintain your account, and enhance your experience with Capt’n.ai. +
  • +
  • + Respond to Your Requests and Inquiries: Your information helps us respond to your customer + service requests, support needs, and other inquiries. +
  • +
  • + Communicate with You: We use your information to communicate with you about the Service, updates, + and other informational or promotional content. +
  • +
  • + Analyze and Monitor Usage: We use analytics tools to track how users interact with the Service, + which helps us make data-driven decisions for improvements. +
  • +
  • + Detect, Investigate, and Prevent Fraud and Other Illegal Activities: We use your information to + protect the security and integrity of the Service by detecting and preventing fraudulent or illegal + activities. +
  • +
+
+ +
+

Authentication And Authorized Data Access

+

+ User authenticates with the 3rd party provider such as Google account. Upon authentication, user allows + the application: +

+
+
    +
  • Associate user with personal info on Google
  • +
  • See user's personal info, including any personal info user made publicly available
  • +
  • View user's email address
  • +
+
+

+ Email address is stored in the database of the application while the other credentials of authenticated + users are encrypted and stored within the infrastructure of Google. This can be used to restrict or fully + block the service for a particular user in case of the breach of the terms of use. User's email + address can be deleted upon the request. +

+
+

Revoke Access to Your Google Account

+ +

+ To remove access of the application to your account, you can do it directly in your Google account by + following this link:{' '} + + https://myaccount.google.com/permissions‍ + +

+
+ +
+

Google API Services User Data Policy

+

Google API Services Disclosure

+

+ Capt’n.ai's use and transfer of information received from Google APIs adhere to{' '} + + Google API Services User Data Policy + + , including the Limited Use requirements. We recommend reviewing Google API Services User Data Policy to + better understand their practices. +

+
+

Use of Google API Services Data

+

+ When you choose to connect various Google services to Capt’n.ai, we require specific permissions to fetch + and display data for your interactive queries. Below are the permissions required for each Google service: +

+
+ +
+

You may choose to connect one, multiple, or none of these services as per your preference.

+
+ +
+

Third-Party and Proprietary AI Tools

+

+ Our chatbot service utilizes advanced AI technology by employing privately deployed OpenAI models on + Microsoft Azure. This approach allows us to generate contextually relevant and accurate responses based on + your interactions and queries, ensuring a high-quality user experience. +

+
+

Data Sharing in Different Use Cases

+

+ Your chat interactions are processed using our privately deployed OpenAI models on Microsoft Azure. This + ensures that your data, including chat history, user metrics, and dimensions from integrated services like + Google Analytics, Google Ads, and Facebook Ads, is not shared with OpenAI directly. +

+
+

Here's a breakdown of the specific data shared from each source:

+
+

+ Google Analytics: Your Google Analytics data includes website traffic information, user behavior, + and engagement metrics from your connected websites. Metrics like page views, session duration, bounce + rate, and user demographics. By incorporating these insights, the chatbot can tailor its responses to + align with the user's website-related inquiries. +

+
+

+ Google Ads: Data from your Google Ads campaigns offers insights into your advertising efforts, ad + performance, and user interactions with your advertisements. Key metrics such as ad clicks, impressions, + click-through rates (CTR), and conversion rates are integrated into the chatbot's learning process. This + integration enables the chatbot to provide more informed and relevant responses regarding your advertising + strategies. +

+
+

+ Google Search Console: Information gathered from Google Search Console sheds light on your + website's visibility in Google search results. Details about search queries, click-through rates (CTR), + and average position help the chatbot understand user intent and prevalent search trends. By leveraging + this data, the chatbot can offer insights and answers that align with current search behaviors. +

+
+

+ Chat Interactions: This refers to the text-based interactions you have with the chatbot within the + Capt’n.ai platform. The content of these conversations, including your questions and responses. This data + aids in refining the AI's ability to comprehend inputs and generate contextually accurate responses. +

+
+

+ All of the data sources mentioned above are crucial for enhancing the chatbot's ability to provide + accurate and contextually relevant responses. When chatting directly on our website, certain data points + from your interactions and connected platforms are processed using our privately deployed OpenAI models on + Microsoft Azure. We ensure that only relevant and necessary data are shared to maintain the effectiveness + of the chatbot's functionality. +

+
+ +
+

Data Storage on Azure Database

+

+ While we do not directly store raw data from third-party sources such as Google Ads, Google Analytics, or + Facebook Ads, it's crucial to understand that your chat history may contain references to or summaries of + data from these services. Retaining this chat history is not just for record-keeping; it's a fundamental + component for the seamless functionality of our chatbot service. This chat data is securely stored in + Azure Database service, a cloud-based database, in compliance with Azure privacy policy ( + + https://learn.microsoft.com/en-us/azure/compliance/ + + ). Your chat history is retained indefinitely, but you have the option to delete it at any time through + the settings in our application. +

+
+ +
+

User Consent Process

+

+ During your registration with Capt’n.ai, we require your explicit consent regarding our privacy practices. + As part of the sign-up process, you will encounter a checkbox indicating that you have read and agree to + our Terms and Conditions and Privacy Policy. By checking this box, you acknowledge your understanding and + agreement to how we handle your data as detailed in these documents. Only upon agreeing to these terms + will the chatbot service proceed with using your data. You have the option to withdraw your consent at any + point, read more below. +

+
+ +
+

Opt-Out Options

+

+ If you choose to withdraw your consent and opt-out of data sharing with third-party tools, you will no + longer be able to use the Capt’n.ai service. The nature of our tool requires data sharing for its basic + functionality. Therefore, opting out effectively means discontinuing use of the service. +

+
+ +
+

Agreement

+

+ By using our chatbot service, you explicitly consent to your chat data being processed as described above. + We ensure that your data is handled securely and in accordance with this privacy policy, as well as Azure + privacy policy. +

+
+

+ If you do not agree with this policy, please refrain from signing up and using Capt’n.ai. +

+
+ +
+

Google Analytics

+

+ Google Analytics is used across captn.ai domain in order to collect information about the users' + interactions with the site as well as to identify returning visits, location, device data and engagement + signals. Collected data helps to understand the relevancy and general usage of the tool hence, to provide + better experience and solutions towards the needs of the users, fix errors and bugs. No data is shared + with the 3rd party organizations or individuals. +

+
+ +
+

Information Sharing and Disclosure

+

We may share your information with third parties in the following circumstances:

+
+
    +
  • + Service Providers: We may share your information with third-party service providers who perform services + on our behalf, such as hosting, analytics, and customer support. +
  • +
  • + AI Data Sharing: Your chat interactions and data from connected third-party services may be shared with + our in-house AI algorithms. This sharing is essential for the service and is detailed in the section + above "Third-Party and Proprietary AI Tools". +
  • +
  • + Compliance with Laws: We may disclose your information as required by law or in response to legal + process, including subpoenas, court orders, and requests from law enforcement. +
  • +
  • + Business Transfers: If we are involved in a merger, acquisition, or sale of all or a portion of our + assets, your information may be transferred as part of that transaction. +
  • +
  • Your Consent: We may disclose your information with your consent.
  • +
+
+ +
+

Your Choices

+

+ You can access and update your account information through the Service. You can also unsubscribe from our + promotional emails by following the instructions in the email. +

+
+ +
+

Data Retention

+

+ We retain the information we collect for as long as necessary to provide the Service and fulfill the + purposes described in this Privacy Policy. When we no longer need the information, we will securely delete + it or de-identify it. Your chat history is retained indefinitely, but you have the option to delete it at + any time through the settings in our application. +

+
+ +
+

Security

+

+ We take reasonable measures to protect your information from unauthorized access, use, disclosure, and + destruction. However, no method of transmission over the internet or method of electronic storage is + completely secure. +

+
+ +
+

Changes to this Privacy Policy

+

+ We may update this Privacy Policy from time to time. If we make any material changes, we will notify you + by email or by posting a notice on our website prior to the change becoming effective. +

+
+ +
+

Contact Us

+

+ In order to receive further information regarding use of the Site, please contact us at:{' '} + + support@captn.ai + + . +

+
+
+
+
+ ); +} diff --git a/app/src/client/app/TocPage.tsx b/app/src/client/app/TocPage.tsx new file mode 100644 index 0000000..8ab75ed --- /dev/null +++ b/app/src/client/app/TocPage.tsx @@ -0,0 +1,197 @@ +export default function TocPage() { + return ( +
+
+
+

Terms & Conditions

+

Last updated January 29, 2024

+ +
+

+ These terms and conditions ("Terms") govern your access to and use of Capt’n.ai, a Software-as-a-Service + tool ("Service") provided by airt technologies, Inc. ("we" or "us"). By accessing or using the Service, + you agree to be bound by these Terms. If you do not agree to these Terms, you may not access or use the + Service. +

+
+ +
+

Use Terms for Capt’n.ai

+

+ Subject to these Terms, we grant you a limited, non-exclusive, non-transferable, revocable license to use + the Service for your internal business purposes during the term of these Terms. +

+
+

+ You may not use the Service in any way that could damage, disable, overburden, or impair the Service or + interfere with any other party's use and enjoyment of the Service. You may not attempt to gain + unauthorized access to the Service or any part of it, other accounts, computer systems, or networks + connected to the Service, through hacking, password mining, or any other means. +

+
+

+ You are solely responsible for all data, information, and content uploaded, stored, or processed using the + Service. You represent and warrant that you have the necessary rights to upload, store, and process such + data, information, and content using the Service and that your use of the Service complies with all + applicable laws, regulations, and industry standards. +

+
+ +
+

Payment and Subscription

+

By subscribing to Capt’n.ai, you agree to the following terms and conditions:

+
+
    +
  • + Subscription: Capt’n.ai is offered on a subscription basis. You will be billed in advance on a + recurring and periodic basis (each period is referred to as a "billing cycle"). Billing cycles are set + either on a monthly or annual basis, depending on the type of subscription plan you select when + purchasing. +
  • +
  • + Payments: All payments are processed by our payment partner, Stripe. By providing a payment + method, you expressly authorize us and Stripe to charge the subscription fees at the start of every + billing cycle. +
  • +
  • + No Refunds: Payments are non-refundable and there are no refunds or credits for partially used + periods. Following any cancellation, however, you will continue to have access to your subscription + through the end of your current billing cycle. +
  • +
  • + Cancellation: You can cancel your subscription at any time. Please note that you must cancel your + subscription before it renews for a subsequent billing cycle in order to avoid being charged for the + next billing cycle. +
  • +
  • + Changes: We reserve the right to change our subscription plans or adjust pricing for our service + in any manner and at any time as we may determine in our sole and absolute discretion. +
  • +
+
+

+ If you have any questions about your Capt’n.ai subscription or these terms, please reach out to us at{' '} + + support@captn.ai + + . +

+
+ +
+

Marketing Emails

+

+ By signing up or creating an account on this website, you agree to receive marketing emails from us, + unless you choose to unsubscribe. These emails may include promotional offers, product updates, + newsletters, or other information related to our services. We value your privacy and assure you that your + email address and personal information will be handled in accordance with our Privacy Policy. +

+
+

+ If you wish to unsubscribe from our marketing emails, you can do so by clicking the "unsubscribe" link + provided at the bottom of each email. Please note that even if you unsubscribe from marketing emails, you + may still receive transactional or account-related communications regarding your use of our services. +

+
+ +
+

Confidentiality

+

+ "Confidential Information" means any information disclosed by either party to the other party that is + marked as confidential or should reasonably be considered confidential given the nature of the information + and the circumstances of its disclosure. +

+
+

+ The recipient of Confidential Information will maintain the confidentiality of the Confidential + Information and will not disclose it to any third party, except as necessary to provide the Service or as + required by law. +

+
+ +
+

Termination

+

+ Either party may terminate these Terms upon written notice to the other party if the other party breaches + any material term of these Terms and fails to cure such breach within thirty (30) days of receiving + written notice of the breach. +

+
+

+ Upon termination of these Terms, you must immediately cease all use of the Service and destroy all copies + of the Service in your possession. +

+
+ +
+

User Data

+

+ We will maintain certain data that you transmit to the Site for the purpose of managing the performance of + the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of + data, you are solely responsible for all data that you transmit or that relates to any activity you have + undertaken using the Site. You agree that we shall have no liability to you for any loss or corruption of + any such data, and you hereby waive any right of action against us arising from any such loss or + corruption of such data. +

+
+ +
+

Disclaimer of Warranties

+

+ The Service is provided "as is" and "as available" without any warranties of any kind, whether express or + implied. +

+
+

+ We do not warrant that the Service will be uninterrupted or error-free, or that the Service will meet your + requirements or expectations. +

+
+

+ We expressly disclaim any and all warranties of merchantability, fitness for a particular purpose, + non-infringement, and any warranties arising out of course of dealing or usage of trade. +

+
+ +
+

Third-Party Website and Content

+

+ The Site may contain (or you may be sent via the Site) links to other websites ("Third-Party Websites") as + well as articles, photographs, text, graphics, pictures, designs, music, sound, video, information, + applications, software, and other content or items belonging to or originating from third parties + ("Third-Party Content"). Such Third-Party Websites and Third-Party Content are not investigated, + monitored, or checked for accuracy, appropriateness, or completeness by us, and we are not responsible for + any Third-Party Websites accessed through the Site or any Third-Party Content posted on, available + through, or installed from the Site, including the content, accuracy, offensiveness, opinions, + reliability, privacy practices, or other policies of or contained in the Third-Party Websites or the + Third-Party Content. Inclusion of, linking to, or permitting the use or installation of any Third-Party + Websites or any Third-Party Content does not imply approval or endorsement thereof by us. If you decide to + leave the Site and access the Third-Party Websites or to use or install any Third-Party Content, you do so + at your own risk, and you should be aware these Terms of Use no longer govern. You should review the + applicable terms and policies, including privacy and data gathering practices, of any website to which you + navigate from the Site or relating to any applications you use or install from the Site. Any purchases you + make through Third-Party Websites will be through other websites and from other companies, and we take no + responsibility whatsoever in relation to such purchases which are exclusively between you and the + applicable third party. You agree and acknowledge that we do not endorse the products or services offered + on Third-Party Websites and you shall hold us harmless from any harm caused by your purchase of such + products or services. Additionally, you shall hold us harmless from any losses sustained by you or harm + caused to you relating to or resulting in any way from any Third-Party Content or any contact with + Third-Party Websites. +

+
+ +
+

Contact Us

+

+ In order to receive further information regarding use of the Site, please contact us at:{' '} + + support@captn.ai + + . +

+
+
+
+
+ ); +} diff --git a/app/src/client/auth/Auth.tsx b/app/src/client/auth/Auth.tsx new file mode 100644 index 0000000..6ed8365 --- /dev/null +++ b/app/src/client/auth/Auth.tsx @@ -0,0 +1,110 @@ +import { type CustomizationOptions } from 'wasp/client/auth'; +import { useState, createContext } from 'react'; +import { createTheme } from '@stitches/react'; +import { styled } from './configs/stitches.config'; +import { LoginSignupForm } from './LoginSignupForm'; + +export enum State { + Login = 'login', + Signup = 'signup', +} + +export type ErrorMessage = { + title: string; + description?: string; +}; + +export const Message = styled('div', { + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + marginTop: '1rem', + background: '$gray400', +}); + +export const MessageSuccess = styled(Message, { + background: '$successBackground', + color: '$successText', +}); + +const logoStyle = { + height: '3rem', +}; + +const Container = styled('div', { + display: 'flex', + flexDirection: 'column', +}); + +// const HeaderText = styled('h2', { +// fontSize: '1.875rem', +// fontWeight: '700', +// marginTop: '1.5rem', +// }); + +export const AuthContext = createContext({ + isLoading: false, + setIsLoading: (isLoading: boolean) => {}, + setErrorMessage: (errorMessage: ErrorMessage | null) => {}, + setSuccessMessage: (successMessage: string | null) => {}, +}); + +const titles: Record = { + login: 'Sign in to your account', + signup: 'Create an account', +}; + +function Auth({ + state, + appearance, + logo, + socialLayout = 'horizontal', + additionalSignupFields, +}: { + state: State; +} & CustomizationOptions & { + additionalSignupFields?: any; + }) { + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // TODO(matija): this is called on every render, is it a problem? + // If we do it in useEffect(), then there is a glitch between the default color and the + // user provided one. + const customTheme = createTheme(appearance ?? {}); + + // const title = titles[state]; + + const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'; + + return ( +
+
+ {logo && Capt’n.ai} + {/* {title} */} +

{state === 'signup' ? titles.signup : titles.login}

+
+ + {/* {errorMessage && ( + + {errorMessage.title} + {errorMessage.description && ': '} + {errorMessage.description} + + )} */} + {successMessage && {successMessage}} + + {(state === 'login' || state === 'signup') && ( + + )} + +
+ ); +} + +export default Auth; diff --git a/app/src/client/auth/LoginPage.tsx b/app/src/client/auth/LoginPage.tsx index 3e579f3..95e8caf 100644 --- a/app/src/client/auth/LoginPage.tsx +++ b/app/src/client/auth/LoginPage.tsx @@ -1,8 +1,15 @@ -import { useAuth, LoginForm } from 'wasp/client/auth'; +import { createTheme } from '@stitches/react'; +import { useAuth } from 'wasp/client/auth'; import { useHistory } from 'react-router-dom'; import { useEffect } from 'react'; -import { Link } from 'react-router-dom'; import { AuthWrapper } from './authWrapper'; +import Auth from './Auth'; +import imgUrl from '../static/logo.svg'; + +export enum State { + Login = 'login', + Signup = 'signup', +} export default function Login() { const history = useHistory(); @@ -17,14 +24,18 @@ export default function Login() { return ( - -
- - Don't have an account yet?{' '} - - go to signup - - +
); } + +export type CustomizationOptions = { + logo?: string; + socialLayout?: 'horizontal' | 'vertical'; + appearance?: Parameters[0]; + state: State; +}; + +export function LoginForm({ appearance, logo, socialLayout, state }: CustomizationOptions) { + return ; +} diff --git a/app/src/client/auth/LoginSignupForm.tsx b/app/src/client/auth/LoginSignupForm.tsx new file mode 100644 index 0000000..f3529cf --- /dev/null +++ b/app/src/client/auth/LoginSignupForm.tsx @@ -0,0 +1,181 @@ +import { useContext, useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; + +import { styled } from './configs/stitches.config'; +import { AuthContext } from './Auth'; +import config from './configs/config'; +import TosAndMarketingEmails from '../components/TosAndMarketingEmails'; +import { State } from './Auth'; +import { Link } from 'wasp/client/router'; + +const SocialAuth = styled('div', { + marginTop: '1.5rem', + marginBottom: '1.5rem', +}); + +const SocialAuthButtons = styled('div', { + marginTop: '0.5rem', + display: 'flex', + + variants: { + direction: { + horizontal: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(48px, 1fr))', + }, + vertical: { + flexDirection: 'column', + margin: '8px 0', + }, + }, + gap: { + small: { + gap: '4px', + }, + medium: { + gap: '8px', + }, + large: { + gap: '16px', + }, + }, + }, +}); + +const googleSignInUrl = `${config.apiUrl}/auth/google/login`; + +export const checkBoxErrMsg = { + title: "To proceed, please ensure you've accepted our Terms & Conditions and Privacy Policy.", + description: '', +}; + +export type LoginSignupFormFields = { + [key: string]: string; +}; + +export const LoginSignupForm = ({ + state, + socialButtonsDirection = 'horizontal', + additionalSignupFields, + errorMessage, +}: { + state: 'login' | 'signup'; + socialButtonsDirection?: 'horizontal' | 'vertical'; + additionalSignupFields?: any; + errorMessage?: any; +}) => { + const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext); + const [tocChecked, setTocChecked] = useState(false); + const [marketingEmailsChecked, setMarketingEmailsChecked] = useState(false); + const [loginFlow, setLoginFlow] = useState(state); + const hookForm = useForm(); + const { + register, + formState: { errors }, + handleSubmit: hookFormHandleSubmit, + } = hookForm; + + useEffect(() => { + if (tocChecked) { + setErrorMessage(null); + } + }, [tocChecked]); + + const handleTocChange = (event: React.ChangeEvent) => { + setTocChecked(event.target.checked); + }; + + const handleMarketingEmailsChange = (event: React.ChangeEvent) => { + setMarketingEmailsChecked(event.target.checked); + }; + + const updateLocalStorage = () => { + localStorage.removeItem('hasAcceptedTos'); + localStorage.removeItem('hasSubscribedToMarketingEmails'); + localStorage.setItem('hasAcceptedTos', JSON.stringify(tocChecked)); + localStorage.setItem('hasSubscribedToMarketingEmails', JSON.stringify(marketingEmailsChecked)); + }; + + const handleClick = (event: React.MouseEvent, googleSignInUrl: string) => { + event.preventDefault(); + if (loginFlow === State.Login) { + updateLocalStorage(); + window.location.href = googleSignInUrl; + } else { + if (tocChecked) { + updateLocalStorage(); + window.location.href = googleSignInUrl; + } else { + setErrorMessage(checkBoxErrMsg); + } + } + }; + + const googleBtnText = loginFlow === State.Login ? 'Sign in with Google' : 'Sign up with Google'; + + return ( + <> + {loginFlow === State.Signup && ( + + )} + + + + + +
+ + {loginFlow === State.Login ? "Don't have an account? " : 'Already have an account? '} + + {loginFlow === State.Login ? 'Sign up' : 'Sign in'} + + +
+ + ); +}; diff --git a/app/src/client/auth/SignupPage.tsx b/app/src/client/auth/SignupPage.tsx index ee75c1c..0281a55 100644 --- a/app/src/client/auth/SignupPage.tsx +++ b/app/src/client/auth/SignupPage.tsx @@ -1,19 +1,11 @@ -import { SignupForm } from 'wasp/client/auth'; -import { Link } from 'react-router-dom'; import { AuthWrapper } from './authWrapper'; +import imgUrl from '../static/logo.svg'; +import { State, LoginForm } from './LoginPage'; export function Signup() { return ( - -
- - I already have an account?{' '} - - go to login - - -
+
); } diff --git a/app/src/client/auth/configs/config.ts b/app/src/client/auth/configs/config.ts new file mode 100644 index 0000000..74c9fde --- /dev/null +++ b/app/src/client/auth/configs/config.ts @@ -0,0 +1,11 @@ +export function stripTrailingSlash(url?: string): string | undefined { + return url?.replace(/\/$/, ""); +} + +const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || 'http://localhost:3001'; + +const config = { + apiUrl, +} + +export default config \ No newline at end of file diff --git a/app/src/client/auth/configs/stitches.config.js b/app/src/client/auth/configs/stitches.config.js new file mode 100644 index 0000000..42b6377 --- /dev/null +++ b/app/src/client/auth/configs/stitches.config.js @@ -0,0 +1,30 @@ +import { createStitches } from "@stitches/react"; + +export const { styled, css } = createStitches({ + theme: { + colors: { + waspYellow: "#ffcc00", + gray700: "#a1a5ab", + gray600: "#d1d5db", + gray500: "gainsboro", + gray400: "#f0f0f0", + red: "#FED7D7", + darkRed: "#fa3838", + green: "#C6F6D5", + + brand: "$waspYellow", + brandAccent: "#ffdb46", + errorBackground: "$red", + errorText: "#2D3748", + successBackground: "$green", + successText: "#2D3748", + + submitButtonText: "black", + + formErrorText: "$darkRed", + }, + fontSizes: { + sm: "0.875rem", + }, + }, +}); diff --git a/app/src/client/components/AnimatedCharacterLoader.tsx b/app/src/client/components/AnimatedCharacterLoader.tsx index d8d3659..93c5e7f 100644 --- a/app/src/client/components/AnimatedCharacterLoader.tsx +++ b/app/src/client/components/AnimatedCharacterLoader.tsx @@ -33,7 +33,7 @@ const AnimatedCharacterLoader: React.FC = ({ >
{showLogo && ( (null); + + const handleChange = (e: React.ChangeEvent) => { + setStatus(e.target.value === 'Yes'); + setHasChanged(true); + }; + + const onClick = () => { + setNotificationType(null); + }; + + const handleClick = async (status: boolean) => { + try { + await updateCurrentUser({ hasSubscribedToMarketingEmails: status }); + setNotificationType('success'); + } catch (e) { + setNotificationType('error'); + } + setHasChanged(false); + }; + + const notificationMsg = + notificationType === 'success' + ? 'Your changes are saved successfully.' + : 'Something went wrong. Please try again later.'; + + return ( + <> + {notificationType && ( + + )} +
+ + +
+ +
+ +
+ + ); +} diff --git a/app/src/client/components/NotificationBox.tsx b/app/src/client/components/NotificationBox.tsx new file mode 100644 index 0000000..bcc71b4 --- /dev/null +++ b/app/src/client/components/NotificationBox.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface NotificationBoxProps { + type: 'success' | 'error'; + message: string; + onClick: () => void; +} + +const NotificationBox: React.FC = ({ type, message, onClick }) => { + const isSuccess = type === 'success'; + + return ( +
+
+

{isSuccess ? 'Success' : 'Error'}

+

{message}

+
+ +
+
+
+ ); +}; + +export default NotificationBox; diff --git a/app/src/client/components/TosAndMarketingEmails.tsx b/app/src/client/components/TosAndMarketingEmails.tsx new file mode 100644 index 0000000..df549b2 --- /dev/null +++ b/app/src/client/components/TosAndMarketingEmails.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { styled } from '../auth/configs/stitches.config'; + +export const Message = styled('div', { + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + marginTop: '1rem', + background: '$gray400', +}); + +export const MessageError = styled(Message, { + background: '#bb6e90', + color: '#eae4d9', +}); + +interface TosAndMarketingEmailsProps { + tocChecked: boolean; + handleTocChange: any; + marketingEmailsChecked: boolean; + handleMarketingEmailsChange: any; + errorMessage: { title: string; description?: string } | null; +} + +const TosAndMarketingEmails: React.FC = ({ + tocChecked, + handleTocChange, + marketingEmailsChecked, + handleMarketingEmailsChange, + errorMessage, +}) => ( +
+
+ +
+
+ +
+ {errorMessage && ( +
+ + {errorMessage.title} + {errorMessage.description && ': '} + {errorMessage.description} + +
+ )} +
+); + +export default TosAndMarketingEmails; diff --git a/app/src/client/components/TosAndMarketingEmailsModal.tsx b/app/src/client/components/TosAndMarketingEmailsModal.tsx new file mode 100644 index 0000000..2de25db --- /dev/null +++ b/app/src/client/components/TosAndMarketingEmailsModal.tsx @@ -0,0 +1,93 @@ +import React, { useState, useEffect, useContext, useMemo } from 'react'; +import { updateCurrentUser } from 'wasp/client/operations'; +import { useHistory } from 'react-router-dom'; + +import { AuthContext } from '../auth/Auth'; +import TosAndMarketingEmails from './TosAndMarketingEmails'; +import { checkBoxErrMsg } from '../auth/LoginSignupForm'; +import AppNavBar from './AppNavBar'; + +export type ErrorMessage = { + title: string; + description?: string; +}; + +export const notificationMsg = + 'Before accessing the application, please confirm your agreement to the Terms & Conditions and Privacy Policy.'; + +const TosAndMarketingEmailsModal = () => { + const history = useHistory(); + const { isLoading, setSuccessMessage, setIsLoading } = useContext(AuthContext); + const [errorMessage, setErrorMessage] = useState(null); + + const [tocChecked, setTocChecked] = useState(false); + const [marketingEmailsChecked, setMarketingEmailsChecked] = useState(false); + + useEffect(() => { + if (tocChecked) { + setErrorMessage(null); + } + }, [tocChecked]); + + const handleTocChange = (event: React.ChangeEvent) => { + setTocChecked(event.target.checked); + }; + + const handleMarketingEmailsChange = (event: React.ChangeEvent) => { + setMarketingEmailsChecked(event.target.checked); + }; + + const onClick = (event: React.MouseEvent) => { + event.preventDefault(); + if (tocChecked) { + setErrorMessage(null); + updateCurrentUser({ + isSignUpComplete: true, + hasAcceptedTos: tocChecked, + ...(marketingEmailsChecked && { + hasSubscribedToMarketingEmails: marketingEmailsChecked, + }), + }); + history.push('/chat'); + } else { + setErrorMessage(checkBoxErrMsg); + } + }; + + const isAccountPage = useMemo(() => { + return location.pathname.startsWith('/account'); + }, [location]); + + return ( + <> + {!isAccountPage && } + +
+
+
+

Almost there...

+

{notificationMsg}

+ + +
+ +
+
+
+
+ + ); +}; + +export default TosAndMarketingEmailsModal; diff --git a/app/src/server/scripts/usersSeed.ts b/app/src/server/scripts/usersSeed.ts index 2046dd8..00e9670 100644 --- a/app/src/server/scripts/usersSeed.ts +++ b/app/src/server/scripts/usersSeed.ts @@ -27,6 +27,9 @@ export function createRandomUser() { credits: faker.number.int({ min: 0, max: 3 }), checkoutSessionId: null, subscriptionTier: faker.helpers.arrayElement([TierIds.HOBBY, TierIds.PRO]), + hasAcceptedTos: true, + hasSubscribedToMarketingEmails: true, + isSignUpComplete: true, }; return user; } diff --git a/app/src/shared/constants.ts b/app/src/shared/constants.ts index 4172a70..192ad3d 100644 --- a/app/src/shared/constants.ts +++ b/app/src/shared/constants.ts @@ -11,7 +11,7 @@ export const BLOG_URL = 'https://docs.opensaas.sh/blog'; const isDevEnv = process.env.NODE_ENV !== 'production'; const customerPortalTestUrl = 'https://billing.stripe.com/p/login/test_4gw7vg40I7vt00wbII'; // TODO: find your test url at https://dashboard.stripe.com/test/settings/billing/portal -const customerPortalProdUrl = ''; // TODO: add before deploying to production +const customerPortalProdUrl = 'https://billing.stripe.com/p/login/3cs8A7ccg7JW5cQ5kk'; // TODO: add before deploying to production export const STRIPE_CUSTOMER_PORTAL_LINK = isDevEnv ? customerPortalTestUrl : customerPortalProdUrl;