diff --git a/.dockerignore b/.dockerignore index 29ff348..7522c89 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,9 @@ node_modules .env /.idea -/cypress/videos -/cypress/screenshots -/coverage # build artifacts /.cache /public/build /build -/server/index.js \ No newline at end of file +/server/index.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9c2116..5efa7de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,40 +25,3 @@ jobs: node-version: 16 - run: npm ci - run: npx tsc - - deploy_development: - if: github.ref == 'refs/heads/master' - needs: [lint, typecheck] - name: Deploy development environment - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: superfly/flyctl-actions@master - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - with: - args: "deploy --strategy rolling -c ./fly.dev.toml" - - uses: appleboy/discord-action@master - with: - webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} - webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} - args: "https://dev.shellphone.app deployed with commit `${{ github.event.head_commit.message }}` (`${{ github.sha }}`) from branch `${{ github.ref }}`" - - - deploy_production: - if: github.ref == 'refs/heads/production' - needs: [lint, typecheck] - name: Deploy production environment - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: superfly/flyctl-actions@master - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - with: - args: "deploy --strategy rolling" - - uses: appleboy/discord-action@master - with: - webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} - webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} - args: "https://www.shellphone.app deployed with commit `${{ github.event.head_commit.message }}` (`${{ github.sha }}`) from branch `${{ github.ref }}`" diff --git a/.gitignore b/.gitignore index 595c16c..eef4f19 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,3 @@ node_modules /.idea .env - -/cypress/videos -/cypress/screenshots -/coverage \ No newline at end of file diff --git a/app/config/config.client.ts b/app/config/config.client.ts deleted file mode 100644 index ff8b4c5..0000000 --- a/app/config/config.client.ts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/app/config/config.server.ts b/app/config/config.server.ts index 025fb72..4baf0b5 100644 --- a/app/config/config.server.ts +++ b/app/config/config.server.ts @@ -40,22 +40,6 @@ invariant( typeof process.env.WEB_PUSH_VAPID_PUBLIC_KEY === "string", `Please define the "WEB_PUSH_VAPID_PUBLIC_KEY" environment variable`, ); -invariant( - typeof process.env.MAILCHIMP_API_KEY === "string", - `Please define the "MAILCHIMP_API_KEY" environment variable`, -); -invariant( - typeof process.env.MAILCHIMP_AUDIENCE_ID === "string", - `Please define the "MAILCHIMP_AUDIENCE_ID" environment variable`, -); -invariant( - typeof process.env.DISCORD_WEBHOOK_ID === "string", - `Please define the "DISCORD_WEBHOOK_ID" environment variable`, -); -invariant( - typeof process.env.DISCORD_WEBHOOK_TOKEN === "string", - `Please define the "DISCORD_WEBHOOK_TOKEN" environment variable`, -); export default { app: { @@ -66,24 +50,11 @@ export default { }, aws: { region: process.env.AWS_REGION, - ses: { - accessKeyId: process.env.AWS_SES_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SES_ACCESS_KEY_SECRET, - fromEmail: process.env.AWS_SES_FROM_EMAIL, - }, s3: { accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_S3_ACCESS_KEY_SECRET, }, }, - discord: { - webhookId: process.env.DISCORD_WEBHOOK_ID, - webhookToken: process.env.DISCORD_WEBHOOK_TOKEN, - }, - mailchimp: { - apiKey: process.env.MAILCHIMP_API_KEY, - audienceId: process.env.MAILCHIMP_AUDIENCE_ID, - }, redis: { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD, diff --git a/app/cron-jobs/daily-backup.ts b/app/cron-jobs/daily-backup.ts deleted file mode 100644 index 0a65e15..0000000 --- a/app/cron-jobs/daily-backup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { CronJob } from "~/utils/queue.server"; -import backup from "~/utils/backup-db.server"; - -export default CronJob("daily db backup", () => backup("daily"), "0 0 * * *"); diff --git a/app/cron-jobs/index.ts b/app/cron-jobs/index.ts deleted file mode 100644 index 50707bc..0000000 --- a/app/cron-jobs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import registerPurgeExpiredSession from "./purge-expired-sessions"; - -export default [registerPurgeExpiredSession]; diff --git a/app/cron-jobs/monthly-backup.ts b/app/cron-jobs/monthly-backup.ts deleted file mode 100644 index a401dae..0000000 --- a/app/cron-jobs/monthly-backup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { CronJob } from "~/utils/queue.server"; -import backup from "~/utils/backup-db.server"; - -export default CronJob("monthly db backup", () => backup("monthly"), "0 0 1 * *"); diff --git a/app/cron-jobs/purge-expired-sessions.ts b/app/cron-jobs/purge-expired-sessions.ts deleted file mode 100644 index 1be6cdc..0000000 --- a/app/cron-jobs/purge-expired-sessions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import db from "~/utils/db.server"; -import { CronJob } from "~/utils/queue.server"; - -export default CronJob( - "purge expired sessions", - async () => { - await db.session.deleteMany({ - where: { - expiresAt: { lt: new Date() }, - }, - }); - }, - "0 0 * * *", -); diff --git a/app/cron-jobs/weekly-backup.ts b/app/cron-jobs/weekly-backup.ts deleted file mode 100644 index e4b9800..0000000 --- a/app/cron-jobs/weekly-backup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { CronJob } from "~/utils/queue.server"; -import backup from "~/utils/backup-db.server"; - -export default CronJob("weekly db backup", () => backup("weekly"), "0 0 * * 0"); diff --git a/app/features/auth/actions/forgot-password.ts b/app/features/auth/actions/forgot-password.ts deleted file mode 100644 index e3546bf..0000000 --- a/app/features/auth/actions/forgot-password.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { type ActionFunction, json } from "@remix-run/node"; -import { type User, TokenType } from "@prisma/client"; - -import db from "~/utils/db.server"; -import { type FormError, validate } from "~/utils/validation.server"; -import { sendForgotPasswordEmail } from "~/mailers/forgot-password-mailer.server"; -import { generateToken, hashToken } from "~/utils/token.server"; -import { ForgotPassword } from "../validations"; - -const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 24; - -type ForgotPasswordFailureActionData = { errors: FormError; submitted?: never }; -type ForgotPasswordSuccessfulActionData = { errors?: never; submitted: true }; -export type ForgotPasswordActionData = ForgotPasswordFailureActionData | ForgotPasswordSuccessfulActionData; - -const action: ActionFunction = async ({ request }) => { - const formData = Object.fromEntries(await request.formData()); - const validation = validate(ForgotPassword, formData); - if (validation.errors) { - return json({ errors: validation.errors }); - } - - const { email } = validation.data; - const user = await db.user.findUnique({ where: { email: email.toLowerCase() } }); - - // always wait the same amount of time so attackers can't tell the difference whether a user is found - await Promise.all([updatePassword(user), new Promise((resolve) => setTimeout(resolve, 750))]); - - // return the same result whether a password reset email was sent or not - return json({ submitted: true }); -}; - -export default action; - -async function updatePassword(user: User | null) { - const membership = await db.membership.findFirst({ where: { userId: user?.id } }); - if (!user || !membership) { - return; - } - - const token = generateToken(); - const hashedToken = hashToken(token); - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS); - - await db.token.deleteMany({ where: { type: TokenType.RESET_PASSWORD, userId: user.id } }); - await db.token.create({ - data: { - user: { connect: { id: user.id } }, - membership: { connect: { id: membership.id } }, - type: TokenType.RESET_PASSWORD, - expiresAt, - hashedToken, - sentTo: user.email, - }, - }); - - await sendForgotPasswordEmail({ - to: user.email, - token, - userName: user.fullName, - }); -} diff --git a/app/features/auth/actions/register.ts b/app/features/auth/actions/register.ts deleted file mode 100644 index 2404ec1..0000000 --- a/app/features/auth/actions/register.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { type ActionFunction, json } from "@remix-run/node"; -import { GlobalRole, MembershipRole } from "@prisma/client"; - -import db from "~/utils/db.server"; -import logger from "~/utils/logger.server"; -import { authenticate, hashPassword } from "~/utils/auth.server"; -import { type FormError, validate } from "~/utils/validation.server"; -import { Register } from "../validations"; - -export type RegisterActionData = { - errors: FormError; -}; - -const action: ActionFunction = async ({ request }) => { - const formData = Object.fromEntries(await request.formData()); - const validation = validate(Register, formData); - if (validation.errors) { - return json({ errors: validation.errors }); - } - - const { fullName, email, password } = validation.data; - const hashedPassword = await hashPassword(password.trim()); - try { - await db.user.create({ - data: { - fullName: fullName.trim(), - email: email.toLowerCase().trim(), - hashedPassword, - role: GlobalRole.CUSTOMER, - memberships: { - create: { - role: MembershipRole.OWNER, - organization: { - create: {} - }, - }, - }, - }, - }); - } catch (error: any) { - logger.error(error); - - if (error.code === "P2002") { - if (error.meta.target[0] === "email") { - return json({ - errors: { general: "An account with this email address already exists" }, - }); - } - } - - return json({ - errors: { general: `An unexpected error happened${error.code ? `\nCode: ${error.code}` : ""}` }, - }); - } - - return authenticate({ email, password, request, failureRedirect: "/register" }); -}; - -export default action; diff --git a/app/features/auth/actions/reset-password.ts b/app/features/auth/actions/reset-password.ts deleted file mode 100644 index 1ddb2b2..0000000 --- a/app/features/auth/actions/reset-password.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { type ActionFunction, json, redirect } from "@remix-run/node"; -import { TokenType } from "@prisma/client"; - -import db from "~/utils/db.server"; -import logger from "~/utils/logger.server"; -import { type FormError, validate } from "~/utils/validation.server"; -import { authenticate, hashPassword } from "~/utils/auth.server"; -import { ResetPasswordError } from "~/utils/errors"; -import { hashToken } from "~/utils/token.server"; -import { ResetPassword } from "../validations"; - -export type ResetPasswordActionData = { errors: FormError }; - -const action: ActionFunction = async ({ request }) => { - const searchParams = new URL(request.url).searchParams; - const token = searchParams.get("token"); - if (!token) { - return redirect("/forgot-password"); - } - - const formData = Object.fromEntries(await request.formData()); - const validation = validate(ResetPassword, { ...formData, token }); - if (validation.errors) { - return json({ errors: validation.errors }); - } - - const hashedToken = hashToken(token); - const savedToken = await db.token.findFirst({ - where: { hashedToken, type: TokenType.RESET_PASSWORD }, - include: { user: true }, - }); - if (!savedToken) { - logger.warn(`No token found with hashedToken=${hashedToken}`); - throw new ResetPasswordError(); - } - - await db.token.delete({ where: { id: savedToken.id } }); - - if (savedToken.expiresAt < new Date()) { - logger.warn(`Token with hashedToken=${hashedToken} is expired since ${savedToken.expiresAt.toUTCString()}`); - throw new ResetPasswordError(); - } - - const password = validation.data.password.trim(); - const hashedPassword = await hashPassword(password); - const { email } = await db.user.update({ - where: { id: savedToken.userId }, - data: { hashedPassword }, - }); - - await db.session.deleteMany({ where: { userId: savedToken.userId } }); - - return authenticate({ email, password, request }); -}; - -export default action; diff --git a/app/features/auth/actions/sign-in.ts b/app/features/auth/actions/sign-in.ts deleted file mode 100644 index b36156f..0000000 --- a/app/features/auth/actions/sign-in.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type ActionFunction, json } from "@remix-run/node"; - -import { SignIn } from "../validations"; -import { type FormError, validate } from "~/utils/validation.server"; -import { authenticate } from "~/utils/auth.server"; - -export type SignInActionData = { errors: FormError }; - -const action: ActionFunction = async ({ request }) => { - const formData = Object.fromEntries(await request.clone().formData()); - const validation = validate(SignIn, formData); - if (validation.errors) { - return json({ errors: validation.errors }); - } - - const searchParams = new URL(request.url).searchParams; - const redirectTo = searchParams.get("redirectTo"); - const successRedirect = redirectTo ? decodeURIComponent(redirectTo) : null; - const { email, password } = validation.data; - return authenticate({ email, password, request, successRedirect }); -}; - -export default action; diff --git a/app/features/auth/loaders/forgot-password.ts b/app/features/auth/loaders/forgot-password.ts deleted file mode 100644 index 0c7edb4..0000000 --- a/app/features/auth/loaders/forgot-password.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { LoaderFunction } from "@remix-run/node"; - -import { requireLoggedOut } from "~/utils/auth.server"; - -const loader: LoaderFunction = async ({ request }) => { - await requireLoggedOut(request); - - return null; -}; - -export default loader; diff --git a/app/features/auth/loaders/register.ts b/app/features/auth/loaders/register.ts deleted file mode 100644 index ce6a626..0000000 --- a/app/features/auth/loaders/register.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type LoaderFunction, json } from "@remix-run/node"; - -import { getErrorMessage, requireLoggedOut } from "~/utils/auth.server"; -import { commitSession, getSession } from "~/utils/session.server"; - -export type RegisterLoaderData = { errors: { general: string } } | null; - -const loader: LoaderFunction = async ({ request }) => { - const session = await getSession(request); - const errorMessage = getErrorMessage(session); - if (errorMessage) { - return json( - { errors: { general: errorMessage } }, - { - headers: { "Set-Cookie": await commitSession(session) }, - }, - ); - } - - await requireLoggedOut(request); - - return null; -}; - -export default loader; diff --git a/app/features/auth/loaders/reset-password.ts b/app/features/auth/loaders/reset-password.ts deleted file mode 100644 index 81975cb..0000000 --- a/app/features/auth/loaders/reset-password.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type LoaderFunction, redirect } from "@remix-run/node"; - -import { requireLoggedOut } from "~/utils/auth.server"; -import { commitSession, getSession } from "~/utils/session.server"; - -const loader: LoaderFunction = async ({ request }) => { - const session = await getSession(request); - const searchParams = new URL(request.url).searchParams; - const token = searchParams.get("token"); - if (!token) { - return redirect("/forgot-password"); - } - - await requireLoggedOut(request); - - return new Response(null, { - headers: { - "Set-Cookie": await commitSession(session), - }, - }); -}; - -export default loader; diff --git a/app/features/auth/loaders/sign-in.ts b/app/features/auth/loaders/sign-in.ts deleted file mode 100644 index 4c04c91..0000000 --- a/app/features/auth/loaders/sign-in.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type LoaderFunction, json } from "@remix-run/node"; - -import { getErrorMessage, requireLoggedOut } from "~/utils/auth.server"; -import { commitSession, getSession } from "~/utils/session.server"; - -export type SignInLoaderData = { errors: { general: string } } | null; - -const loader: LoaderFunction = async ({ request }) => { - const session = await getSession(request); - const errorMessage = getErrorMessage(session); - if (errorMessage) { - return json( - { errors: { general: errorMessage } }, - { - headers: { "Set-Cookie": await commitSession(session) }, - }, - ); - } - - await requireLoggedOut(request); - - return null; -}; - -export default loader; diff --git a/app/features/auth/pages/forgot-password.tsx b/app/features/auth/pages/forgot-password.tsx deleted file mode 100644 index a1107ad..0000000 --- a/app/features/auth/pages/forgot-password.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Form, useActionData, useTransition } from "@remix-run/react"; - -import type { ForgotPasswordActionData } from "../actions/forgot-password"; -import LabeledTextField from "~/features/core/components/labeled-text-field"; -import Button from "~/features/core/components/button"; - -export default function ForgotPasswordPage() { - const actionData = useActionData(); - const transition = useTransition(); - const isSubmitting = transition.state === "submitting"; - - return ( -
-
-

- Forgot your password? -

-
- -
- {actionData?.submitted ? ( -

- If your email is in our system, you will receive instructions to reset your password shortly. -

- ) : ( - <> - - - - - )} - -
- ); -} diff --git a/app/features/auth/pages/register.tsx b/app/features/auth/pages/register.tsx deleted file mode 100644 index 5ae4dcc..0000000 --- a/app/features/auth/pages/register.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Form, Link, useActionData, useLoaderData, useTransition } from "@remix-run/react"; - -import type { RegisterActionData } from "../actions/register"; -import type { RegisterLoaderData } from "../loaders/register"; -import LabeledTextField from "~/features/core/components/labeled-text-field"; -import Alert from "~/features/core/components/alert"; -import Button from "~/features/core/components/button"; - -export default function RegisterPage() { - const loaderData = useLoaderData(); - const actionData = useActionData(); - const transition = useTransition(); - const isSubmitting = transition.state === "submitting"; - const topErrorMessage = loaderData?.errors?.general || actionData?.errors?.general; - - return ( -
-
-

- Create your account -

-

- - Already have an account? - -

-
- -
- {topErrorMessage ? ( -
- -
- ) : null} - - - - - - - -
- ); -} diff --git a/app/features/auth/pages/reset-password.tsx b/app/features/auth/pages/reset-password.tsx deleted file mode 100644 index 258245a..0000000 --- a/app/features/auth/pages/reset-password.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Form, useActionData, useSearchParams, useTransition } from "@remix-run/react"; -import clsx from "clsx"; - -import type { ResetPasswordActionData } from "../actions/reset-password"; -import LabeledTextField from "~/features/core/components/labeled-text-field"; - -export default function ForgotPasswordPage() { - const [searchParams] = useSearchParams(); - const actionData = useActionData(); - const transition = useTransition(); - const isSubmitting = transition.state === "submitting"; - - return ( -
-
-

Set a new password

-
- -
- - - - - - -
- ); -} diff --git a/app/features/auth/pages/sign-in.tsx b/app/features/auth/pages/sign-in.tsx deleted file mode 100644 index 0485f4d..0000000 --- a/app/features/auth/pages/sign-in.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Form, Link, useActionData, useLoaderData, useSearchParams, useTransition } from "@remix-run/react"; - -import type { SignInActionData } from "../actions/sign-in"; -import type { SignInLoaderData } from "../loaders/sign-in"; -import LabeledTextField from "~/features/core/components/labeled-text-field"; -import Alert from "~/features/core/components/alert"; -import Button from "~/features/core/components/button"; - -export default function SignInPage() { - const [searchParams] = useSearchParams(); - const loaderData = useLoaderData(); - const actionData = useActionData(); - const transition = useTransition(); - const isSubmitting = transition.state === "submitting"; - return ( -
-
-

Welcome back!

- {/*

- Need an account?  - - Create yours for free - -

*/} -
- -
- {loaderData?.errors ? ( -
- -
- ) : null} - - - - - Forgot your password? - - } - /> - - - -
- ); -} diff --git a/app/features/auth/validations.ts b/app/features/auth/validations.ts deleted file mode 100644 index 9a469b5..0000000 --- a/app/features/auth/validations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod"; - -export const password = z.string().min(10).max(100); - -export const Register = z.object({ - fullName: z.string().nonempty(), - email: z.string().email(), - password, -}); - -export const SignIn = z.object({ - email: z.string().email(), - password, -}); - -export const ForgotPassword = z.object({ - email: z.string().email(), -}); - -export const ResetPassword = z - .object({ - password: password, - passwordConfirmation: password, - token: z.string(), - }) - .refine((data) => data.password === data.passwordConfirmation, { - message: "Passwords don't match", - path: ["passwordConfirmation"], // set the path of the error - }); - -export const AcceptInvitation = z.object({ - fullName: z.string(), - email: z.string().email(), - password, - token: z.string(), -}); - -export const AcceptAuthedInvitation = z.object({ - token: z.string(), -}); diff --git a/app/features/core/actions/notifications-subscription.ts b/app/features/core/actions/notifications-subscription.ts index 4cd8431..2fa6c9e 100644 --- a/app/features/core/actions/notifications-subscription.ts +++ b/app/features/core/actions/notifications-subscription.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import db from "~/utils/db.server"; import logger from "~/utils/logger.server"; import { validate } from "~/utils/validation.server"; -import { requireLoggedIn } from "~/utils/auth.server"; +import { getSession } from "~/utils/session.server"; const action: ActionFunction = async ({ request }) => { const formData = await request.clone().formData(); @@ -31,7 +31,6 @@ const action: ActionFunction = async ({ request }) => { export default action; async function subscribe(request: Request) { - const { organization } = await requireLoggedIn(request); const formData = await request.formData(); const body = { subscription: JSON.parse(formData.get("subscription")?.toString() ?? "{}"), @@ -42,17 +41,16 @@ async function subscribe(request: Request) { } const { subscription } = validation.data; - const membership = await db.membership.findFirst({ - where: { id: organization.membershipId }, - }); - if (!membership) { - return notFound("Phone number not found"); + const session = await getSession(request); + const twilio = session.get("twilio"); + if (!twilio) { + throw new Error("unreachable"); } try { await db.notificationSubscription.create({ data: { - membershipId: membership.id, + twilioAccountSid: twilio.accountSid, endpoint: subscription.endpoint, expirationTime: subscription.expirationTime, keys_p256dh: subscription.keys.p256dh, diff --git a/app/features/core/components/button.tsx b/app/features/core/components/button.tsx deleted file mode 100644 index ab33a1f..0000000 --- a/app/features/core/components/button.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ButtonHTMLAttributes, FunctionComponent } from "react"; -import { useTransition } from "@remix-run/react"; -import clsx from "clsx"; - -type Props = ButtonHTMLAttributes; - -const Button: FunctionComponent = ({ children, ...props }) => { - const transition = useTransition(); - - return ( - - ); -} - -export default Button; diff --git a/app/features/core/components/inactive-subscription.tsx b/app/features/core/components/inactive-subscription.tsx deleted file mode 100644 index 800498a..0000000 --- a/app/features/core/components/inactive-subscription.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useNavigate } from "@remix-run/react"; -import { IoSettings, IoAlertCircleOutline } from "react-icons/io5"; - -export default function InactiveSubscription() { - const navigate = useNavigate(); - - return ( -
- -
-
-
-
-
- ); -} diff --git a/app/features/core/components/select.tsx b/app/features/core/components/select.tsx deleted file mode 100644 index b4de76c..0000000 --- a/app/features/core/components/select.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Fragment } from "react"; -import { Listbox, Transition } from "@headlessui/react"; -import { HiCheck as CheckIcon, HiSelector as SelectorIcon } from "react-icons/hi"; -import clsx from "clsx"; - -type Option = { name: string; value: string }; - -type Props = { - options: Option[]; - onChange: (selectedValue: Option) => void; - value: Option; -}; - -export default function Select({ options, onChange, value }: Props) { - return ( - -
- - {value.name} - - - - - - {options.map((option, index) => ( - - clsx( - "cursor-default select-none relative py-2 pl-10 pr-4", - active ? "text-amber-900 bg-amber-100" : "text-gray-900", - ) - } - value={option} - > - {({ selected, active }) => ( - <> - - {option.name} - - {selected ? ( - - - ) : null} - - )} - - ))} - - -
-
- ); -} diff --git a/app/features/core/components/spinner.css b/app/features/core/components/spinner.css deleted file mode 100644 index 2bc3648..0000000 --- a/app/features/core/components/spinner.css +++ /dev/null @@ -1,15 +0,0 @@ -.ring { - display: inline-block; - width: 50px; - height: 50px; - border: 3px solid rgba(0, 0, 0, 0.15); - border-radius: 50%; - border-top-color: currentColor; - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} diff --git a/app/features/core/components/spinner.tsx b/app/features/core/components/spinner.tsx deleted file mode 100644 index 509c93d..0000000 --- a/app/features/core/components/spinner.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { LinksFunction } from "@remix-run/node"; - -import styles from "./spinner.css"; - -export const links: LinksFunction = () => [ - { rel: "stylesheet", href: styles }, -]; - -export default function Spinner() { - return ( -
-
-
- ); -} diff --git a/app/features/keypad/loaders/keypad.ts b/app/features/keypad/loaders/keypad.ts index c0d2d98..4e460bb 100644 --- a/app/features/keypad/loaders/keypad.ts +++ b/app/features/keypad/loaders/keypad.ts @@ -2,21 +2,20 @@ import type { LoaderFunction } from "@remix-run/node"; import { json } from "superjson-remix"; import { Prisma } from "@prisma/client"; -import { requireLoggedIn } from "~/utils/auth.server"; import db from "~/utils/db.server"; +import { getSession } from "~/utils/session.server"; export type KeypadLoaderData = { - hasOngoingSubscription: boolean; hasPhoneNumber: boolean; lastRecipientCalled?: string; }; const loader: LoaderFunction = async ({ request }) => { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); const phoneNumber = await db.phoneNumber.findUnique({ where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio?.accountSid ?? "", isCurrent: true } }, }); - const hasOngoingSubscription = true; // TODO const hasPhoneNumber = Boolean(phoneNumber); const lastCall = phoneNumber && @@ -26,7 +25,6 @@ const loader: LoaderFunction = async ({ request }) => { })); return json( { - hasOngoingSubscription, hasPhoneNumber, lastRecipientCalled: lastCall?.recipient, }, diff --git a/app/features/messages/actions/messages.$recipient.tsx b/app/features/messages/actions/messages.$recipient.tsx index c2aa8ca..ba8f47c 100644 --- a/app/features/messages/actions/messages.$recipient.tsx +++ b/app/features/messages/actions/messages.$recipient.tsx @@ -2,19 +2,21 @@ import { type ActionFunction } from "@remix-run/node"; import { json } from "superjson-remix"; import db from "~/utils/db.server"; -import { requireLoggedIn } from "~/utils/auth.server"; import getTwilioClient, { translateMessageDirection, translateMessageStatus } from "~/utils/twilio.server"; +import { getSession } from "~/utils/session.server"; -export type NewMessageActionData = {}; +type NewMessageActionData = {}; const action: ActionFunction = async ({ params, request }) => { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { throw new Error("unreachable"); } + const [phoneNumber, twilioAccount] = await Promise.all([ db.phoneNumber.findUnique({ - where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio.accountSid ?? "", isCurrent: true } }, + where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio.accountSid, isCurrent: true } }, }), db.twilioAccount.findUnique({ where: { accountSid: twilio.accountSid } }), ]); diff --git a/app/features/messages/loaders/messages.$recipient.ts b/app/features/messages/loaders/messages.$recipient.ts index 05a5b26..23a0482 100644 --- a/app/features/messages/loaders/messages.$recipient.ts +++ b/app/features/messages/loaders/messages.$recipient.ts @@ -4,8 +4,8 @@ import { parsePhoneNumber } from "awesome-phonenumber"; import { type Message, type PhoneNumber, Prisma } from "@prisma/client"; import db from "~/utils/db.server"; -import { requireLoggedIn } from "~/utils/auth.server"; import { redirect } from "@remix-run/node"; +import { getSession } from "~/utils/session.server"; type ConversationType = { recipient: string; @@ -19,7 +19,8 @@ export type ConversationLoaderData = { }; const loader: LoaderFunction = async ({ request, params }) => { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { return redirect("/messages"); } diff --git a/app/features/messages/loaders/messages.ts b/app/features/messages/loaders/messages.ts index de56e29..f15239d 100644 --- a/app/features/messages/loaders/messages.ts +++ b/app/features/messages/loaders/messages.ts @@ -4,7 +4,7 @@ import { parsePhoneNumber } from "awesome-phonenumber"; import { type Message, type PhoneNumber, Prisma } from "@prisma/client"; import db from "~/utils/db.server"; -import { requireLoggedIn } from "~/utils/auth.server"; +import { getSession } from "~/utils/session.server"; export type MessagesLoaderData = { hasPhoneNumber: boolean; @@ -19,7 +19,8 @@ type Conversation = { }; const loader: LoaderFunction = async ({ request }) => { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); const phoneNumber = await db.phoneNumber.findUnique({ where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio?.accountSid ?? "", isCurrent: true } }, }); diff --git a/app/features/phone-calls/components/phone-calls-list.tsx b/app/features/phone-calls/components/phone-calls-list.tsx index 4587ebb..1aad4e3 100644 --- a/app/features/phone-calls/components/phone-calls-list.tsx +++ b/app/features/phone-calls/components/phone-calls-list.tsx @@ -10,20 +10,14 @@ import { formatRelativeDate } from "~/features/core/helpers/date-formatter"; import type { PhoneCallsLoaderData } from "~/features/phone-calls/loaders/calls"; export default function PhoneCallsList() { - const { hasOngoingSubscription, isFetchingCalls, phoneCalls } = useLoaderData(); + const { isFetchingCalls, phoneCalls } = useLoaderData(); - if (!hasOngoingSubscription) { - if (!phoneCalls || phoneCalls.length === 0) { - return null; - } - } else { - if (isFetchingCalls || !phoneCalls) { - return ; - } + if (isFetchingCalls || !phoneCalls) { + return ; + } - if (phoneCalls.length === 0) { - return hasOngoingSubscription ? : null; - } + if (phoneCalls.length === 0) { + return ; } return ( diff --git a/app/features/phone-calls/loaders/calls.ts b/app/features/phone-calls/loaders/calls.ts index 630451e..d218671 100644 --- a/app/features/phone-calls/loaders/calls.ts +++ b/app/features/phone-calls/loaders/calls.ts @@ -4,7 +4,7 @@ import { parsePhoneNumber } from "awesome-phonenumber"; import { type PhoneCall, Prisma } from "@prisma/client"; import db from "~/utils/db.server"; -import { requireLoggedIn } from "~/utils/auth.server"; +import { getSession } from "~/utils/session.server"; type PhoneCallMeta = { formattedPhoneNumber: string; @@ -12,7 +12,6 @@ type PhoneCallMeta = { }; export type PhoneCallsLoaderData = { - hasOngoingSubscription: boolean; hasPhoneNumber: boolean; } & ( | { @@ -26,15 +25,14 @@ export type PhoneCallsLoaderData = { ); const loader: LoaderFunction = async ({ request }) => { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); const phoneNumber = await db.phoneNumber.findUnique({ where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio?.accountSid ?? "", isCurrent: true } }, }); const hasPhoneNumber = Boolean(phoneNumber); - const hasOngoingSubscription = true; // TODO if (!phoneNumber || phoneNumber.isFetchingCalls) { return json({ - hasOngoingSubscription, hasPhoneNumber, isFetchingCalls: phoneNumber?.isFetchingCalls ?? false, }); @@ -46,7 +44,6 @@ const loader: LoaderFunction = async ({ request }) => { }); return json( { - hasOngoingSubscription, hasPhoneNumber, phoneCalls: phoneCalls.map((phoneCall) => ({ ...phoneCall, diff --git a/app/features/phone-calls/loaders/twilio-token.ts b/app/features/phone-calls/loaders/twilio-token.ts index 5b74c14..4748fa0 100644 --- a/app/features/phone-calls/loaders/twilio-token.ts +++ b/app/features/phone-calls/loaders/twilio-token.ts @@ -1,17 +1,17 @@ import { type LoaderFunction } from "@remix-run/node"; import Twilio from "twilio"; -import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server"; import { decrypt, encrypt } from "~/utils/encryption"; import db from "~/utils/db.server"; -import { commitSession } from "~/utils/session.server"; +import { getSession } from "~/utils/session.server"; import getTwilioClient from "~/utils/twilio.server"; import logger from "~/utils/logger.server"; export type TwilioTokenLoaderData = string; const loader: LoaderFunction = async ({ request }) => { - const { user, twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { logger.warn("Twilio account is not connected"); return null; @@ -26,7 +26,6 @@ const loader: LoaderFunction = async ({ request }) => { } const twilioClient = getTwilioClient(twilioAccount); - let shouldRefreshSession = false; let { apiKeySid, apiKeySecret } = twilioAccount; if (apiKeySid && apiKeySecret) { try { @@ -41,7 +40,6 @@ const loader: LoaderFunction = async ({ request }) => { } } if (!apiKeySid || !apiKeySecret) { - shouldRefreshSession = true; const apiKey = await twilioClient.newKeys.create({ friendlyName: "Shellphone" }); apiKeySid = apiKey.sid; apiKeySecret = encrypt(apiKey.secret); @@ -52,7 +50,7 @@ const loader: LoaderFunction = async ({ request }) => { } const accessToken = new Twilio.jwt.AccessToken(twilioAccount.accountSid, apiKeySid, decrypt(apiKeySecret), { - identity: `${twilio.accountSid}__${user.id}`, + identity: `shellphone__${twilio.accountSid}`, ttl: 3600, }); const grant = new Twilio.jwt.AccessToken.VoiceGrant({ @@ -62,11 +60,6 @@ const loader: LoaderFunction = async ({ request }) => { accessToken.addGrant(grant); const headers = new Headers({ "Content-Type": "text/plain" }); - if (shouldRefreshSession) { - const { session } = await refreshSessionData(request); - headers.set("Set-Cookie", await commitSession(session)); - } - return new Response(accessToken.toJwt(), { headers }); }; diff --git a/app/features/public-area/actions/index.ts b/app/features/public-area/actions/index.ts deleted file mode 100644 index e2fcf77..0000000 --- a/app/features/public-area/actions/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type ActionFunction, json } from "@remix-run/node"; - -import { addSubscriber } from "~/utils/mailchimp.server"; -import { executeWebhook } from "~/utils/discord.server"; - -export type JoinWaitlistActionData = { submitted: true }; - -const action: ActionFunction = async ({ request }) => { - const formData = await request.formData(); - const email = formData.get("email"); - if (!formData.get("email") || typeof email !== "string") { - throw new Error("Something wrong happened"); - } - - // await addSubscriber(email); - const res = await executeWebhook(email); - console.log(res.status); - console.log(await res.text()); - - return json({ submitted: true }); -}; - -export default action; diff --git a/app/features/public-area/components/button.tsx b/app/features/public-area/components/button.tsx deleted file mode 100644 index 3b86bc8..0000000 --- a/app/features/public-area/components/button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { ButtonHTMLAttributes } from "react"; -import clsx from "clsx"; - -const baseStyles = { - solid: "group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2", - outline: "group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none", -}; - -const variantStyles = { - solid: { - slate: "bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900", - primary: - "bg-primary-600 text-white hover:text-slate-100 hover:bg-primary-500 active:bg-primary-800 active:text-primary-100 focus-visible:outline-primary-600", - white: "bg-white text-slate-900 hover:bg-primary-50 active:bg-primary-200 active:text-slate-600 focus-visible:outline-white", - }, - outline: { - slate: "ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-primary-600 focus-visible:ring-slate-300", - white: "ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white", - }, -}; - -type Props = ButtonHTMLAttributes & - ( - | { - variant: "solid"; - color: "slate" | "primary" | "white"; - } - | { - variant: "outline"; - color: "slate" | "white"; - } - ) & { - className?: string; - }; - -export default function Button({ variant, color, className, ...props }: Props) { - // @ts-ignore - const fullClassName = clsx(baseStyles[variant], variantStyles[variant][color], className); - - return - - )} - - - - ); -} diff --git a/app/features/public-area/components/container.tsx b/app/features/public-area/components/container.tsx deleted file mode 100644 index aa49c61..0000000 --- a/app/features/public-area/components/container.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { HTMLAttributes } from "react"; -import clsx from "clsx"; - -type Props = HTMLAttributes & { - className?: string; -}; - -export default function Container({ className, ...props }: Props) { - return
; -} diff --git a/app/features/public-area/components/faqs.tsx b/app/features/public-area/components/faqs.tsx deleted file mode 100644 index 771eeb5..0000000 --- a/app/features/public-area/components/faqs.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import type { FunctionComponent, PropsWithChildren } from "react"; -import { Disclosure, Transition } from "@headlessui/react"; -import clsx from "clsx"; - -import Container from "./container"; - -import backgroundImage from "../images/background-faqs.webp"; - -export default function Faqs() { - return ( -
- - -
-

- Frequently asked questions -

-
-
    - - Shellphone is your go-to app to use your phone number over the internet. It integrates - seamlessly with Twilio to provide the best experience for your personal cloud phone. - - - Shellphone is still in its early stages and we're working hard to make it as easy-to-use as - possible. Currently, you must have a Twilio account to set up your personal cloud phone with - Shellphone. - - - Chances are you're currently using an eSIM-compatible device. eSIMs are a reasonable way of - using a phone number internationally but they are still subject to some irky limitations. For - example, you can only use an eSIM on one device at a time and you are still subject to - exorbitant rates from your carrier. - - - Some banks and online services refuse to send two-factor authentication messages to a virtual - phone number and we do not have a solution around this yet. Moreover, Twilio does not support - receiving incoming SMS from external Alphanumeric Sender IDs is to protect accounts getting - bombarded from spam messages from these IDs which are used to send one-way SMS. -
    - With that said, we have successfully received 2FA messages from many services including WhatsApp - and Uber. We recognize this is a common problem for people who want to switch to a virtual phone - number and we are doing our best to find a long-term solution to receive 2FA messages. -
    -
-
-
- ); -} - -function FAQs() { - return ( -
-
-
-

Questions & Answers

-
- -
    - - Shellphone is your go-to app to use your phone number over the internet. It integrates - seamlessly with Twilio to provide the best experience for your personal cloud phone. - - - Shellphone is still in its early stages and we're working hard to make it as easy-to-use as - possible. Currently, you must have a Twilio account to set up your personal cloud phone with - Shellphone. - - - Chances are you're currently using an eSIM-compatible device. eSIMs are a reasonable way of - using a phone number internationally but they are still subject to some irky limitations. For - example, you can only use an eSIM on one device at a time and you are still subject to - exorbitant rates from your carrier. - -
-
-
- ); -} - -const Accordion: FunctionComponent> = ({ title, children }) => { - return ( - - {({ open }) => ( - <> - - - - - - {title} - - - - -

{children}

-
-
- - )} -
- ); -}; diff --git a/app/features/public-area/components/fields.tsx b/app/features/public-area/components/fields.tsx deleted file mode 100644 index b8f6661..0000000 --- a/app/features/public-area/components/fields.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { InputHTMLAttributes, HTMLAttributes, PropsWithChildren } from "react"; - -const formClasses = - "block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm"; - -function Label({ id, children }: PropsWithChildren>) { - return ( - - ); -} - -export function TextField({ - id, - label, - type = "text", - className = "", - ...props -}: InputHTMLAttributes & { label?: string }) { - return ( -
- {label && } - -
- ); -} diff --git a/app/features/public-area/components/header.tsx b/app/features/public-area/components/header.tsx deleted file mode 100644 index fa71974..0000000 --- a/app/features/public-area/components/header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Link } from "@remix-run/react"; - -import Button from "./button"; -import Container from "./container"; -import Logo from "./logo"; -import NavLink from "./nav-link"; - -export default function Header() { - return ( -
- - - -
- ); -} diff --git a/app/features/public-area/components/hero.tsx b/app/features/public-area/components/hero.tsx deleted file mode 100644 index 6f8173f..0000000 --- a/app/features/public-area/components/hero.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Button from "./button"; -import Container from "./container"; - -/* -height: calc(100vh - 120px); -display: flex; -flex-direction: column; -justify-content: center; -margin-top: -120px; - */ - -export default function Hero() { - return ( - -

- - Calling your bank from abroad - {" "} - just got{" "} - - - easier - {" "} - ! -

-

- Coming soon, the personal cloud phone for digital nomads! Take your phone number anywhere you go 🌏 -

-
- ); -} diff --git a/app/features/public-area/components/logo.tsx b/app/features/public-area/components/logo.tsx deleted file mode 100644 index 1e860a6..0000000 --- a/app/features/public-area/components/logo.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Logo() { - return Shellphone logo; -} diff --git a/app/features/public-area/components/nav-link.tsx b/app/features/public-area/components/nav-link.tsx deleted file mode 100644 index cf90fa7..0000000 --- a/app/features/public-area/components/nav-link.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { PropsWithChildren } from "react"; -import { Link } from "@remix-run/react"; - -export default function NavLink({ href, children }: PropsWithChildren<{ href: string }>) { - return ( - - {children} - - ); -} diff --git a/app/features/public-area/images/background-call-to-action.jpg b/app/features/public-area/images/background-call-to-action.jpg deleted file mode 100644 index 13d8ee5..0000000 Binary files a/app/features/public-area/images/background-call-to-action.jpg and /dev/null differ diff --git a/app/features/public-area/images/background-call-to-action.webp b/app/features/public-area/images/background-call-to-action.webp deleted file mode 100644 index 98a6284..0000000 Binary files a/app/features/public-area/images/background-call-to-action.webp and /dev/null differ diff --git a/app/features/public-area/images/background-faqs.jpg b/app/features/public-area/images/background-faqs.jpg deleted file mode 100644 index d9de04f..0000000 Binary files a/app/features/public-area/images/background-faqs.jpg and /dev/null differ diff --git a/app/features/public-area/images/background-faqs.webp b/app/features/public-area/images/background-faqs.webp deleted file mode 100644 index 3144848..0000000 Binary files a/app/features/public-area/images/background-faqs.webp and /dev/null differ diff --git a/app/features/public-area/pages/index.tsx b/app/features/public-area/pages/index.tsx deleted file mode 100644 index 9f89e31..0000000 --- a/app/features/public-area/pages/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Header from "../components/header"; -import Hero from "../components/hero"; -import CallToAction from "../components/call-to-action"; -import Faqs from "../components/faqs"; - -export default function IndexPage() { - return ( -
-
-
- - - -
-
- ); -} diff --git a/app/features/settings/actions/account.ts b/app/features/settings/actions/account.ts deleted file mode 100644 index 0ea46fe..0000000 --- a/app/features/settings/actions/account.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { type ActionFunction, json, redirect } from "@remix-run/node"; -import { badRequest } from "remix-utils"; -import { z } from "zod"; -import SecurePassword from "secure-password"; - -import db from "~/utils/db.server"; -import logger from "~/utils/logger.server"; -import { hashPassword, requireLoggedIn, verifyPassword } from "~/utils/auth.server"; -import { type FormError, validate } from "~/utils/validation.server"; -import { destroySession, getSession } from "~/utils/session.server"; -import deleteUserQueue from "~/queues/delete-user-data.server"; - -const action: ActionFunction = async ({ request }) => { - const formData = Object.fromEntries(await request.formData()); - if (!formData._action) { - const errorMessage = "POST /settings/phone without any _action"; - logger.error(errorMessage); - return badRequest({ errorMessage }); - } - - switch (formData._action as Action) { - case "deleteUser": - return deleteUser(request); - case "changePassword": - return changePassword(request, formData); - case "updateUser": - return updateUser(request, formData); - default: - const errorMessage = `POST /settings/phone with an invalid _action=${formData._action}`; - logger.error(errorMessage); - return badRequest({ errorMessage }); - } -}; - -export default action; - -async function deleteUser(request: Request) { - const { - user: { id }, - } = await requireLoggedIn(request); - - await db.user.update({ - where: { id }, - data: { hashedPassword: "pending deletion" }, - }); - await deleteUserQueue.add(`delete user ${id}`, { userId: id }); - - return redirect("/", { - headers: { - "Set-Cookie": await destroySession(await getSession(request)), - }, - }); -} - -type ChangePasswordFailureActionData = { errors: FormError; submitted?: never }; -type ChangePasswordSuccessfulActionData = { errors?: never; submitted: true }; -export type ChangePasswordActionData = { - changePassword: ChangePasswordFailureActionData | ChangePasswordSuccessfulActionData; -}; - -async function changePassword(request: Request, formData: unknown) { - const validation = validate(validations.changePassword, formData); - if (validation.errors) { - return json({ - changePassword: { errors: validation.errors }, - }); - } - - const { - user: { id }, - } = await requireLoggedIn(request); - const user = await db.user.findUnique({ where: { id } }); - const { currentPassword, newPassword } = validation.data; - const verificationResult = await verifyPassword(user!.hashedPassword!, currentPassword); - if ([SecurePassword.INVALID, SecurePassword.INVALID_UNRECOGNIZED_HASH, false].includes(verificationResult)) { - return json({ - changePassword: { errors: { currentPassword: "Current password is incorrect" } }, - }); - } - - const hashedPassword = await hashPassword(newPassword.trim()); - await db.user.update({ - where: { id: user!.id }, - data: { hashedPassword }, - }); - - return json({ - changePassword: { submitted: true }, - }); -} - -type UpdateUserFailureActionData = { errors: FormError; submitted?: never }; -type UpdateUserSuccessfulActionData = { errors?: never; submitted: true }; -export type UpdateUserActionData = { - updateUser: UpdateUserFailureActionData | UpdateUserSuccessfulActionData; -}; - -async function updateUser(request: Request, formData: unknown) { - const validation = validate(validations.updateUser, formData); - if (validation.errors) { - return json({ - updateUser: { errors: validation.errors }, - }); - } - - const { user } = await requireLoggedIn(request); - const { email, fullName } = validation.data; - await db.user.update({ - where: { id: user.id }, - data: { email, fullName }, - }); - - return json({ - updateUser: { submitted: true }, - }); -} - -type Action = "deleteUser" | "updateUser" | "changePassword"; - -const validations = { - deleteUser: null, - changePassword: z.object({ - currentPassword: z.string(), - newPassword: z.string().min(10).max(100), - }), - updateUser: z.object({ - fullName: z.string(), - email: z.string(), - }), -} as const; diff --git a/app/features/settings/actions/phone.ts b/app/features/settings/actions/phone.ts index 87e06d4..87d290b 100644 --- a/app/features/settings/actions/phone.ts +++ b/app/features/settings/actions/phone.ts @@ -5,8 +5,7 @@ import type { Prisma } from "@prisma/client"; import db from "~/utils/db.server"; import { type FormActionData, validate } from "~/utils/validation.server"; -import { refreshSessionData, requireLoggedIn } from "~/utils/auth.server"; -import { commitSession } from "~/utils/session.server"; +import { commitSession, getSession } from "~/utils/session.server"; import setTwilioWebhooksQueue from "~/queues/set-twilio-webhooks.server"; import logger from "~/utils/logger.server"; import { encrypt } from "~/utils/encryption"; @@ -40,7 +39,8 @@ const action: ActionFunction = async ({ request }) => { export type SetPhoneNumberActionData = FormActionData; async function setPhoneNumber(request: Request, formData: unknown) { - const { organization, twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { return badRequest({ setPhoneNumber: { @@ -72,7 +72,6 @@ async function setPhoneNumber(request: Request, formData: unknown) { }); await setTwilioWebhooksQueue.add(`set twilio webhooks for phoneNumberId=${validation.data.phoneNumberSid}`, { phoneNumberId: validation.data.phoneNumberSid, - organizationId: organization.id, }); return json({ setPhoneNumber: { submitted: true } }); @@ -81,7 +80,8 @@ async function setPhoneNumber(request: Request, formData: unknown) { export type SetTwilioCredentialsActionData = FormActionData; async function setTwilioCredentials(request: Request, formData: unknown) { - const { organization, twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); const validation = validate(validations.setTwilioCredentials, formData); if (validation.errors) { return badRequest({ setTwilioCredentials: { errors: validation.errors } }); @@ -99,10 +99,10 @@ async function setTwilioCredentials(request: Request, formData: unknown) { throw error; } - let session: Session | undefined; if (twilio) { + console.log("fail"); await db.twilioAccount.delete({ where: { accountSid: twilio?.accountSid } }); - session = (await refreshSessionData(request)).session; + session.unset("twilio"); } return json( @@ -112,11 +112,9 @@ async function setTwilioCredentials(request: Request, formData: unknown) { }, }, { - headers: session - ? { - "Set-Cookie": await commitSession(session), - } - : {}, + headers: { + "Set-Cookie": await commitSession(session), + }, }, ); } @@ -128,13 +126,8 @@ async function setTwilioCredentials(request: Request, formData: unknown) { const [phoneNumbers] = await Promise.all([ twilioClient.incomingPhoneNumbers.list(), db.twilioAccount.upsert({ - where: { organizationId: organization.id }, - create: { - organization: { - connect: { id: organization.id }, - }, - ...data, - }, + where: { accountSid: twilioAccountSid }, + create: data, update: data, }), ]); @@ -143,11 +136,11 @@ async function setTwilioCredentials(request: Request, formData: unknown) { accountSid: twilioAccountSid, }); await Promise.all( - phoneNumbers.map(async (phoneNumber) => { + phoneNumbers.map(async (phoneNumber, index) => { const phoneNumberId = phoneNumber.sid; logger.info(`Importing phone number with id=${phoneNumberId}`); try { - await db.phoneNumber.create({ + await db.phoneNumber.createMany({ data: { id: phoneNumberId, twilioAccountSid, @@ -156,6 +149,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) { isFetchingCalls: true, isFetchingMessages: true, }, + skipDuplicates: true, }); await Promise.all([ @@ -177,19 +171,25 @@ async function setTwilioCredentials(request: Request, formData: unknown) { }), ); - const { session } = await refreshSessionData(request); + session.set("twilio", { accountSid: twilioAccountSid, authToken }); + console.log("{ accountSid: twilioAccountSid, authToken }", { accountSid: twilioAccountSid, authToken }); + console.log("session", session.get("twilio"), session.data); + const setCookie = await commitSession(session); + console.log("set twilio in session", setCookie); + return json( { setTwilioCredentials: { submitted: true } }, { headers: { - "Set-Cookie": await commitSession(session), + "Set-Cookie": setCookie, }, }, ); } async function refreshPhoneNumbers(request: Request) { - const { twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { throw new Error("unreachable"); } diff --git a/app/features/settings/components/account/danger-zone.tsx b/app/features/settings/components/account/danger-zone.tsx deleted file mode 100644 index c8fc24e..0000000 --- a/app/features/settings/components/account/danger-zone.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useRef, useState } from "react"; -import { Form, useTransition } from "@remix-run/react"; -import clsx from "clsx"; - -import Button from "../button"; -import SettingsSection from "../settings-section"; -import Modal, { ModalTitle } from "~/features/core/components/modal"; - -export default function DangerZone() { - const transition = useTransition(); - const isCurrentFormTransition = transition.submission?.formData.get("_action") === "deleteUser"; - const isDeletingUser = isCurrentFormTransition && transition.state === "submitting"; - const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); - const modalCancelButtonRef = useRef(null); - - const closeModal = () => { - if (isDeletingUser) { - return; - } - - setIsConfirmationModalOpen(false); - }; - - return ( - -
-

- Once you delete your account, all of its data will be permanently deleted and any ongoing - subscription will be cancelled. -

- - - - -
- - -
-
- Delete my account -
-

- Are you sure you want to delete your account? Your subscription will be cancelled and - your data permanently deleted. -

-

- You are free to create a new account with the same email address if you ever wish to - come back. -

-
-
-
-
-
- - -
- -
-
-
- ); -} diff --git a/app/features/settings/components/account/profile-informations.tsx b/app/features/settings/components/account/profile-informations.tsx deleted file mode 100644 index 6dca7a2..0000000 --- a/app/features/settings/components/account/profile-informations.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { FunctionComponent } from "react"; -import { Form, useActionData, useTransition } from "@remix-run/react"; - -import type { UpdateUserActionData } from "~/features/settings/actions/account"; -import useSession from "~/features/core/hooks/use-session"; -import Alert from "~/features/core/components/alert"; -import Button from "../button"; -import SettingsSection from "../settings-section"; - -const ProfileInformations: FunctionComponent = () => { - const { user } = useSession(); - const transition = useTransition(); - const actionData = useActionData()?.updateUser; - - const errors = actionData?.errors; - const topErrorMessage = errors?.general; - const isError = typeof topErrorMessage !== "undefined"; - const isSuccess = actionData?.submitted; - const isCurrentFormTransition = transition.submission?.formData.get("_action") === "updateUser"; - const isSubmitting = isCurrentFormTransition && transition.state === "submitting"; - - return ( -
- - -
- } - > - {isError ? ( -
- -
- ) : null} - - {isSuccess && ( -
- -
- )} - -
- -
- -
-
- -
- -
- -
-
- - - - - ); -}; - -export default ProfileInformations; diff --git a/app/features/settings/components/account/update-password.tsx b/app/features/settings/components/account/update-password.tsx deleted file mode 100644 index 4762774..0000000 --- a/app/features/settings/components/account/update-password.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { FunctionComponent } from "react"; -import { Form, useActionData, useTransition } from "@remix-run/react"; - -import type { ChangePasswordActionData } from "~/features/settings/actions/account"; -import Alert from "~/features/core/components/alert"; -import LabeledTextField from "~/features/core/components/labeled-text-field"; -import Button from "../button"; -import SettingsSection from "../settings-section"; - -const UpdatePassword: FunctionComponent = () => { - const transition = useTransition(); - const actionData = useActionData()?.changePassword; - - const topErrorMessage = actionData?.errors?.general; - const isError = typeof topErrorMessage !== "undefined"; - const isSuccess = actionData?.submitted; - const isCurrentFormTransition = transition.submission?.formData.get("_action") === "changePassword"; - const isSubmitting = isCurrentFormTransition && transition.state === "submitting"; - - return ( -
- - -
- } - > - {isError ? ( -
- -
- ) : null} - - {isSuccess ? ( -
- -
- ) : null} - - - - - - - - - ); -}; - -export default UpdatePassword; diff --git a/app/features/settings/components/billing/billing-history.tsx b/app/features/settings/components/billing/billing-history.tsx deleted file mode 100644 index 82f9f59..0000000 --- a/app/features/settings/components/billing/billing-history.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { IoChevronBack, IoChevronForward } from "react-icons/io5"; -import clsx from "clsx"; - -import usePaymentsHistory from "../../hooks/use-payments-history"; - -export default function BillingHistory() { - const { - payments, - count, - skip, - pagesNumber, - currentPage, - lastPage, - hasPreviousPage, - hasNextPage, - goToPreviousPage, - goToNextPage, - setPage, - } = usePaymentsHistory(); - - if (payments.length === 0) { - return null; - } - - return ( -
-
-

Billing history

-
-
-
-
-
- - - - - - - - - - - {payments.map((payment) => ( - - - - - - - ))} - -
- Date - - Amount - - Status - - View receipt -
- - - {Intl.NumberFormat(undefined, { - style: "currency", - currency: payment.currency, - currencyDisplay: "narrowSymbol", - }).format(payment.amount)} - - {payment.is_paid === 1 ? "Paid" : "Upcoming"} - - {typeof payment.receipt_url !== "undefined" ? ( - - View receipt - - ) : null} -
- -
-
- -

- Page {currentPage} of{" "} - {lastPage} -

- -
-
-
-

- Showing {skip + 1} to{" "} - {skip + payments.length} of{" "} - {count} results -

-
-
- -
-
-
-
-
-
-
-
- ); -} diff --git a/app/features/settings/components/billing/paddle-link.tsx b/app/features/settings/components/billing/paddle-link.tsx deleted file mode 100644 index 567df50..0000000 --- a/app/features/settings/components/billing/paddle-link.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FunctionComponent, MouseEventHandler } from "react"; -import { HiExternalLink } from "react-icons/hi"; - -type Props = { - onClick: MouseEventHandler; - text: string; -}; - -const PaddleLink: FunctionComponent = ({ onClick, text }) => ( - -); - -export default PaddleLink; diff --git a/app/features/settings/components/billing/plans.tsx b/app/features/settings/components/billing/plans.tsx deleted file mode 100644 index 4b2ea4a..0000000 --- a/app/features/settings/components/billing/plans.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useState } from "react"; -import clsx from "clsx"; -import { type Subscription, SubscriptionStatus } from "@prisma/client"; - -import SwitchPlanModal from "./switch-plan-modal"; - -export type Plan = typeof pricing["tiers"][number]; - -function useSubscription() { - return { - hasActiveSubscription: false, - subscription: null as any, - subscribe: () => void 0, - changePlan: () => void 0, - }; -} - -export default function Plans() { - const { hasActiveSubscription, subscription, subscribe, changePlan } = useSubscription(); - const [nextPlan, setNextPlan] = useState(null); - const [isSwitchPlanModalOpen, setIsSwitchPlanModalOpen] = useState(false); - - return ( - <> -
- {pricing.tiers.map((tier) => { - const isCurrentTier = subscription?.paddlePlanId === tier.planId; - const isActiveTier = hasActiveSubscription && isCurrentTier; - const cta = getCTA({ subscription, tier }); - - return ( -
-
-

{tier.title}

- {tier.yearly ? ( -

- Get 2 months free! -

- ) : null} -

- {tier.price}€ - {tier.frequency} -

- {tier.yearly ? ( -

Billed yearly ({tier.price * 12}€)

- ) : null} -

{tier.description}

-
- - -
- ); - })} -
- - { - // changePlan({ planId: nextPlan.planId }); - // Panelbear.track(`Subscribe to ${nextPlan.title}`); - setIsSwitchPlanModalOpen(false); - }} - closeModal={() => setIsSwitchPlanModalOpen(false)} - /> - - ); -} - -function getCTA({ - subscription, - tier, -}: { - subscription?: Subscription; - tier: typeof pricing["tiers"][number]; -}): string { - if (!subscription) { - return "Subscribe"; - } - - const isCancelling = subscription.status === SubscriptionStatus.deleted; - if (isCancelling) { - return "Resubscribe"; - } - - const isCurrentTier = subscription.paddlePlanId === tier.planId; - const hasActiveSubscription = subscription.status !== SubscriptionStatus.deleted; - const isActiveTier = hasActiveSubscription && isCurrentTier; - if (isActiveTier) { - return "Current plan"; - } - - return `Switch to ${tier.title}`; -} - -const pricing = { - tiers: [ - { - title: "Yearly", - planId: 727544, - price: 12.5, - frequency: "/month", - description: "Text and call anyone, anywhere in the world, all year long.", - yearly: true, - }, - { - title: "Monthly", - planId: 727540, - price: 15, - frequency: "/month", - description: "Text and call anyone, anywhere in the world.", - yearly: false, - }, - ], -}; diff --git a/app/features/settings/components/billing/switch-plan-modal.tsx b/app/features/settings/components/billing/switch-plan-modal.tsx deleted file mode 100644 index ad758eb..0000000 --- a/app/features/settings/components/billing/switch-plan-modal.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { FunctionComponent } from "react"; -import { useRef } from "react"; - -import Modal, { ModalTitle } from "~/features/core/components/modal"; -import type { Plan } from "./plans"; - -type Props = { - isOpen: boolean; - nextPlan: Plan | null; - confirm: (nextPlan: Plan) => void; - closeModal: () => void; -}; - -const SwitchPlanModal: FunctionComponent = ({ isOpen, nextPlan, confirm, closeModal }) => { - const confirmButtonRef = useRef(null); - - return ( - -
-
- Are you sure you want to switch to {nextPlan?.title}? -
-

- You're about to switch to the {nextPlan?.title} plan. You will be - billed immediately a prorated amount and the next billing date will be recalculated from - today. -

-
-
-
-
- - -
-
- ); -}; - -export default SwitchPlanModal; diff --git a/app/features/settings/components/phone/help-modal.tsx b/app/features/settings/components/phone/help-modal.tsx index f037ce7..3ac8536 100644 --- a/app/features/settings/components/phone/help-modal.tsx +++ b/app/features/settings/components/phone/help-modal.tsx @@ -16,19 +16,6 @@ const HelpModal: FunctionComponent = ({ isHelpModalOpen, closeModal }) =>
Need some help?
-

- Try{" "} - - reconnecting your Twilio account - to refresh the phone numbers. -

-

- If you are stuck, pick a date & time on{" "} - - our calendly - {" "} - and we will help you get started! -

Don't miss out on free $10 Twilio credit by using{" "} diff --git a/app/features/settings/components/phone/phone-number-form.tsx b/app/features/settings/components/phone/phone-number-form.tsx index b9eb080..40559a1 100644 --- a/app/features/settings/components/phone/phone-number-form.tsx +++ b/app/features/settings/components/phone/phone-number-form.tsx @@ -25,7 +25,7 @@ export default function PhoneNumberForm() { const topErrorMessage = errors?.general ?? errors?.phoneNumberSid; const isError = typeof topErrorMessage !== "undefined"; const currentPhoneNumber = availablePhoneNumbers.find((phoneNumber) => phoneNumber.isCurrent === true); - const hasFilledTwilioCredentials = twilio !== null; + const hasFilledTwilioCredentials = twilio != null; if (!hasFilledTwilioCredentials) { return null; diff --git a/app/features/settings/components/phone/twilio-connect.tsx b/app/features/settings/components/phone/twilio-connect.tsx index 2ee76e1..8f0b5b5 100644 --- a/app/features/settings/components/phone/twilio-connect.tsx +++ b/app/features/settings/components/phone/twilio-connect.tsx @@ -13,9 +13,11 @@ import Button from "~/features/settings/components/button"; export default function TwilioConnect() { const { twilio } = useSession(); + console.log("twilio", twilio); const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); const transition = useTransition(); - const actionData = useActionData()?.setTwilioCredentials; + const actionData = useActionData() + ?.setTwilioCredentials as SetTwilioCredentialsActionData["setTwilioCredentials"]; const { accountSid, authToken } = useLoaderData(); const topErrorMessage = actionData?.errors?.general; @@ -50,7 +52,7 @@ export default function TwilioConnect() {

- {twilio !== null ? ( + {twilio != null ? (

✓ Your Twilio account is connected to Shellphone.

) : null} diff --git a/app/features/settings/loaders/phone.ts b/app/features/settings/loaders/phone.ts index a724016..c871853 100644 --- a/app/features/settings/loaders/phone.ts +++ b/app/features/settings/loaders/phone.ts @@ -2,9 +2,9 @@ import { type LoaderArgs, json } from "@remix-run/node"; import { type PhoneNumber, Prisma } from "@prisma/client"; import db from "~/utils/db.server"; -import { requireLoggedIn } from "~/utils/auth.server"; import logger from "~/utils/logger.server"; import { decrypt } from "~/utils/encryption"; +import { getSession } from "~/utils/session.server"; export type PhoneSettingsLoaderData = { accountSid?: string; @@ -13,14 +13,15 @@ export type PhoneSettingsLoaderData = { }; const loader = async ({ request }: LoaderArgs) => { - const { organization, twilio } = await requireLoggedIn(request); + const session = await getSession(request); + const twilio = session.get("twilio"); if (!twilio) { logger.warn("Twilio account is not connected"); return json({ phoneNumbers: [] }); } const phoneNumbers = await db.phoneNumber.findMany({ - where: { twilioAccount: { organizationId: organization.id } }, + where: { twilioAccount: { accountSid: twilio.accountSid } }, select: { id: true, number: true, isCurrent: true }, orderBy: { id: Prisma.SortOrder.desc }, }); diff --git a/app/mailers/forgot-password-mailer.server.ts b/app/mailers/forgot-password-mailer.server.ts deleted file mode 100644 index 6dfbab9..0000000 --- a/app/mailers/forgot-password-mailer.server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import sendEmail from "~/utils/mailer.server"; -import serverConfig from "~/config/config.server"; -import { render } from "./renderer/renderer.server"; - -type Params = { - to: string; - token: string; - userName: string; -}; - -export async function sendForgotPasswordEmail({ to, token, userName }: Params) { - const origin = serverConfig.app.baseUrl; - const resetUrl = `${origin}/reset-password?token=${token}`; - const html = await render("forgot-password", { action_url: resetUrl, name: userName }); - - return sendEmail({ - recipients: to, - subject: "Reset your password", - html, - }); -} diff --git a/app/mailers/renderer/html/components/footer.html b/app/mailers/renderer/html/components/footer.html deleted file mode 100644 index be2e686..0000000 --- a/app/mailers/renderer/html/components/footer.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/app/mailers/renderer/html/components/header.html b/app/mailers/renderer/html/components/header.html deleted file mode 100644 index 665367f..0000000 --- a/app/mailers/renderer/html/components/header.html +++ /dev/null @@ -1,15 +0,0 @@ - - -
- - diff --git a/app/mailers/renderer/html/custom/postmark/buttons.css b/app/mailers/renderer/html/custom/postmark/buttons.css deleted file mode 100644 index 053f490..0000000 --- a/app/mailers/renderer/html/custom/postmark/buttons.css +++ /dev/null @@ -1,32 +0,0 @@ -.button { - @apply inline-block text-white no-underline; - background-color: #3869d4; - border-top: 10px solid #3869d4; - border-right: 18px solid #3869d4; - border-bottom: 10px solid #3869d4; - border-left: 18px solid #3869d4; - border-radius: 3px; - box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); -} - -.button--green { - background-color: #22bc66; - border-top: 10px solid #22bc66; - border-right: 18px solid #22bc66; - border-bottom: 10px solid #22bc66; - border-left: 18px solid #22bc66; -} - -.button--red { - background-color: #ff6136; - border-top: 10px solid #ff6136; - border-right: 18px solid #ff6136; - border-bottom: 10px solid #ff6136; - border-left: 18px solid #ff6136; -} - -@screen sm { - .button { - @apply w-full text-center !important; - } -} diff --git a/app/mailers/renderer/html/custom/postmark/index.css b/app/mailers/renderer/html/custom/postmark/index.css deleted file mode 100644 index 31770c3..0000000 --- a/app/mailers/renderer/html/custom/postmark/index.css +++ /dev/null @@ -1,65 +0,0 @@ -@import "buttons"; - -.purchase_heading { - border-bottom-width: 1px; - border-bottom-color: #eaeaec; - border-bottom-style: solid; -} - -.purchase_heading p { - @apply text-xxs leading-24 m-0; - color: #85878e; -} - -.purchase_footer { - @apply pt-16 text-base align-middle; - border-top-width: 1px; - border-top-color: #eaeaec; - border-top-style: solid; -} - -.body-sub { - @apply mt-25 pt-25 border-t; - border-top-color: #eaeaec; - border-top-style: solid; -} - -.discount { - @apply w-full p-24 bg-gray-postmark-lightest; - border: 2px dashed #cbcccf; -} - -.email-masthead { - @apply py-24 text-base text-center; -} - -@screen dark { - body, - .email-body, - .email-body_inner, - .email-content, - .email-wrapper, - .email-masthead, - .email-footer { - @apply bg-gray-postmark-darker text-white !important; - } - - p, - ul, - ol, - blockquote, - h1, - h2, - h3 { - @apply text-white !important; - } - - .attributes_content, - .discount { - @apply bg-gray-postmark-darkest !important; - } - - .email-masthead_name { - text-shadow: none !important; - } -} diff --git a/app/mailers/renderer/html/custom/reset.css b/app/mailers/renderer/html/custom/reset.css deleted file mode 100644 index 84bd249..0000000 --- a/app/mailers/renderer/html/custom/reset.css +++ /dev/null @@ -1,10 +0,0 @@ -body { - @apply m-0 p-0 w-full; - word-break: break-word; - -webkit-font-smoothing: antialiased; -} - -img { - border: 0; - @apply max-w-full leading-full align-middle; -} diff --git a/app/mailers/renderer/html/custom/utilities.css b/app/mailers/renderer/html/custom/utilities.css deleted file mode 100644 index 94a73b0..0000000 --- a/app/mailers/renderer/html/custom/utilities.css +++ /dev/null @@ -1,3 +0,0 @@ -.mso-leading-exactly { - mso-line-height-rule: exactly; -} diff --git a/app/mailers/renderer/html/layouts/main.html b/app/mailers/renderer/html/layouts/main.html deleted file mode 100644 index ffc1980..0000000 --- a/app/mailers/renderer/html/layouts/main.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - {{{ page.title }}} - - - - - - - - - - - - - - -
- -
- - diff --git a/app/mailers/renderer/html/templates/forgot-password.html b/app/mailers/renderer/html/templates/forgot-password.html deleted file mode 100644 index 7295f49..0000000 --- a/app/mailers/renderer/html/templates/forgot-password.html +++ /dev/null @@ -1,93 +0,0 @@ ---- -bodyClass: bg-gray-postmark-lighter ---- - - - - - - - - - - diff --git a/app/mailers/renderer/html/templates/tailwind.css b/app/mailers/renderer/html/templates/tailwind.css deleted file mode 100644 index 74e5146..0000000 --- a/app/mailers/renderer/html/templates/tailwind.css +++ /dev/null @@ -1,18 +0,0 @@ -/* Your custom CSS resets for email */ -@import "app/mailers/renderer/html/custom/reset"; - -/* Tailwind components that are generated by plugins */ -@import "tailwindcss/components"; - -/** - * @import here any custom components - classes that you'd want loaded - * before the Tailwind utilities, so that the utilities could still - * override them. -*/ -@import "app/mailers/renderer/html/custom/postmark"; - -/* Tailwind utility classes */ -@import "tailwindcss/utilities"; - -/* Your custom utility classes */ -@import "app/mailers/renderer/html/custom/utilities"; diff --git a/app/mailers/renderer/html/templates/team-invitation.html b/app/mailers/renderer/html/templates/team-invitation.html deleted file mode 100644 index b564853..0000000 --- a/app/mailers/renderer/html/templates/team-invitation.html +++ /dev/null @@ -1,81 +0,0 @@ ---- -bodyClass: bg-gray-postmark-lighter ---- - - - - - - - - - - diff --git a/app/mailers/renderer/renderer.server.ts b/app/mailers/renderer/renderer.server.ts deleted file mode 100644 index 6c0293d..0000000 --- a/app/mailers/renderer/renderer.server.ts +++ /dev/null @@ -1,219 +0,0 @@ -import fs from "fs"; -import path from "path"; -// @ts-ignore -import Maizzle from "@maizzle/framework"; - -export async function render(templateName: string, locals: Record = {}) { - const { template, options } = getMaizzleParams(templateName, locals); - const { html } = await Maizzle.render(template, options); - - return html; -} - -function getMaizzleParams(templateName: string, locals: Record) { - const template = fs - .readFileSync(path.resolve(process.cwd(), "./app/mailers/renderer/html/templates", `${templateName}.html`)) - .toString(); - const tailwindCss = fs - .readFileSync(path.resolve(process.cwd(), "./app/mailers/renderer/html/templates/tailwind.css")) - .toString(); - - const options = { - tailwind: { - css: tailwindCss, - config: { - mode: "jit", - theme: { - screens: { - sm: { max: "600px" }, - dark: { raw: "(prefers-color-scheme: dark)" }, - }, - extend: { - colors: { - gray: { - "postmark-lightest": "#F4F4F7", - "postmark-lighter": "#F2F4F6", - "postmark-light": "#A8AAAF", - "postmark-dark": "#51545E", - "postmark-darker": "#333333", - "postmark-darkest": "#222222", - "postmark-meta": "#85878E", - }, - blue: { - postmark: "#3869D4", - }, - }, - spacing: { - screen: "100vw", - full: "100%", - px: "1px", - 0: "0", - 2: "2px", - 3: "3px", - 4: "4px", - 5: "5px", - 6: "6px", - 7: "7px", - 8: "8px", - 9: "9px", - 10: "10px", - 11: "11px", - 12: "12px", - 14: "14px", - 16: "16px", - 20: "20px", - 21: "21px", - 24: "24px", - 25: "25px", - 28: "28px", - 30: "30px", - 32: "32px", - 35: "35px", - 36: "36px", - 40: "40px", - 44: "44px", - 45: "45px", - 48: "48px", - 52: "52px", - 56: "56px", - 60: "60px", - 64: "64px", - 72: "72px", - 80: "80px", - 96: "96px", - 570: "570px", - 600: "600px", - "1/2": "50%", - "1/3": "33.333333%", - "2/3": "66.666667%", - "1/4": "25%", - "2/4": "50%", - "3/4": "75%", - "1/5": "20%", - "2/5": "40%", - "3/5": "60%", - "4/5": "80%", - "1/6": "16.666667%", - "2/6": "33.333333%", - "3/6": "50%", - "4/6": "66.666667%", - "5/6": "83.333333%", - "1/12": "8.333333%", - "2/12": "16.666667%", - "3/12": "25%", - "4/12": "33.333333%", - "5/12": "41.666667%", - "6/12": "50%", - "7/12": "58.333333%", - "8/12": "66.666667%", - "9/12": "75%", - "10/12": "83.333333%", - "11/12": "91.666667%", - }, - borderRadius: { - none: "0px", - sm: "2px", - DEFAULT: "4px", - md: "6px", - lg: "8px", - xl: "12px", - "2xl": "16px", - "3xl": "24px", - full: "9999px", - }, - fontFamily: { - sans: ['"Nunito Sans"', "-apple-system", '"Segoe UI"', "sans-serif"], - serif: ["Constantia", "Georgia", "serif"], - mono: ["Menlo", "Consolas", "monospace"], - }, - fontSize: { - 0: "0", - xxs: "12px", - xs: "13px", - sm: "14px", - base: "16px", - lg: "18px", - xl: "20px", - "2xl": "24px", - "3xl": "30px", - "4xl": "36px", - "5xl": "48px", - "6xl": "60px", - "7xl": "72px", - "8xl": "96px", - "9xl": "128px", - }, - inset: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - letterSpacing: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - lineHeight: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - maxHeight: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - maxWidth: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - xs: "160px", - sm: "192px", - md: "224px", - lg: "256px", - xl: "288px", - "2xl": "336px", - "3xl": "384px", - "4xl": "448px", - "5xl": "512px", - "6xl": "576px", - "7xl": "640px", - }), - minHeight: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - minWidth: (theme: TailwindThemeHelper) => ({ - ...theme("spacing"), - }), - }, - }, - corePlugins: { - animation: false, - backgroundOpacity: false, - borderOpacity: false, - divideOpacity: false, - placeholderOpacity: false, - textOpacity: false, - }, - }, - }, - maizzle: { - build: { - posthtml: { - expressions: { - locals, - }, - }, - }, - company: { - name: "Capsule Corp.", - address: `
39 Robinson Rd, #11-01
Singapore 068911`, - product: "Remixtape", - sender: "Mokhtar", - mailto: "mokhtar@remixtape.dev", - }, - googleFonts: "family=Nunito+Sans:wght@400;700", - year: () => new Date().getFullYear(), - inlineCSS: true, - prettify: true, - removeUnusedCSS: true, - }, - }; - - return { - template, - options, - }; -} - -type TailwindThemeHelper = (str: string) => {}; diff --git a/app/mailers/team-invitation-mailer.server.ts b/app/mailers/team-invitation-mailer.server.ts deleted file mode 100644 index 0cebce0..0000000 --- a/app/mailers/team-invitation-mailer.server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import sendEmail from "~/utils/mailer.server"; -import serverConfig from "~/config/config.server"; -import { render } from "./renderer/renderer.server"; - -type Params = { - to: string; - token: string; - userName: string; - organizationName: string; -}; - -export async function sendTeamInvitationEmail({ to, token, userName, organizationName }: Params) { - const origin = serverConfig.app.baseUrl; - const invitationUrl = `${origin}/accept-invitation?token=${token}`; - const html = await render("team-invitation", { - action_url: invitationUrl, - invitation_sender_name: userName, - invitation_sender_organization_name: organizationName, - }); - - return sendEmail({ - recipients: to, - subject: `${userName} has invited you to work with them in Remixtape`, - html, - }); -} diff --git a/app/queues/delete-user-data.server.ts b/app/queues/delete-user-data.server.ts deleted file mode 100644 index c34de22..0000000 --- a/app/queues/delete-user-data.server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MembershipRole } from "@prisma/client"; - -import { Queue } from "~/utils/queue.server"; -import db from "~/utils/db.server"; -import logger from "~/utils/logger.server"; -import { deleteOrganizationEntities } from "~/utils/organization.server"; - -type Payload = { - userId: string; -}; - -export default Queue("delete user data", async ({ data }) => { - const { userId } = data; - const user = await db.user.findUnique({ - where: { id: userId }, - include: { - memberships: { - include: { organization: true }, - }, - }, - }); - if (!user) { - return; - } - - await Promise.all( - user.memberships.map(async (membership) => { - switch (membership.role) { - case MembershipRole.OWNER: { - await deleteOrganizationEntities(membership.organization); - break; - } - case MembershipRole.USER: { - await db.membership.delete({ where: { id: membership.id } }); - break; - } - } - }), - ); - - try { - await db.user.delete({ where: { id: user.id } }); - } catch (error: any) { - if (error.code === "P2025") { - logger.warn("Could not delete user because it has already been deleted"); - return; - } - - throw error; - } -}); diff --git a/app/queues/index.ts b/app/queues/index.ts index 9d04937..3925f19 100644 --- a/app/queues/index.ts +++ b/app/queues/index.ts @@ -1,4 +1,3 @@ -import deleteUserDataQueue from "./delete-user-data.server"; import fetchPhoneCallsQueue from "./fetch-phone-calls.server"; import insertPhoneCallsQueue from "./insert-phone-calls.server"; import fetchMessagesQueue from "./fetch-messages.server"; @@ -7,7 +6,6 @@ import setTwilioWebhooksQueue from "./set-twilio-webhooks.server"; import setTwilioApiKeyQueue from "./set-twilio-api-key.server"; export default [ - deleteUserDataQueue, fetchPhoneCallsQueue, insertPhoneCallsQueue, fetchMessagesQueue, diff --git a/app/queues/notify-incoming-message.server.ts b/app/queues/notify-incoming-message.server.ts index f41a199..0ad8bfd 100644 --- a/app/queues/notify-incoming-message.server.ts +++ b/app/queues/notify-incoming-message.server.ts @@ -16,15 +16,7 @@ export default Queue("notify incoming message", async ({ data }) => { where: { id: phoneNumberId }, select: { twilioAccount: { - include: { - organization: { - select: { - memberships: { - select: { notificationSubscription: true }, - }, - }, - }, - }, + include: { notificationSubscriptions: true }, }, }, }); @@ -32,10 +24,7 @@ export default Queue("notify incoming message", async ({ data }) => { logger.warn(`No phone number found with id=${phoneNumberId}`); return; } - const subscriptions = phoneNumber.twilioAccount.organization.memberships.flatMap( - (membership) => membership.notificationSubscription, - ); - + const subscriptions = phoneNumber.twilioAccount.notificationSubscriptions; const twilioClient = getTwilioClient(phoneNumber.twilioAccount); const message = await twilioClient.messages.get(messageSid).fetch(); const payload = buildMessageNotificationPayload(message); diff --git a/app/queues/set-twilio-webhooks.server.ts b/app/queues/set-twilio-webhooks.server.ts index e072fe5..716d577 100644 --- a/app/queues/set-twilio-webhooks.server.ts +++ b/app/queues/set-twilio-webhooks.server.ts @@ -8,13 +8,12 @@ import { decrypt } from "~/utils/encryption"; type Payload = { phoneNumberId: string; - organizationId: string; }; export default Queue("set twilio webhooks", async ({ data }) => { - const { phoneNumberId, organizationId } = data; + const { phoneNumberId } = data; const phoneNumber = await db.phoneNumber.findFirst({ - where: { id: phoneNumberId, twilioAccount: { organizationId } }, + where: { id: phoneNumberId }, include: { twilioAccount: { select: { accountSid: true, twimlAppSid: true, authToken: true }, @@ -33,7 +32,7 @@ export default Queue("set twilio webhooks", async ({ data }) => { await Promise.all([ db.twilioAccount.update({ - where: { organizationId }, + where: { accountSid: twilioAccount.accountSid }, data: { twimlAppSid }, }), twilioClient.incomingPhoneNumbers.get(phoneNumber.id).update({ diff --git a/app/routes/__app.tsx b/app/routes/__app.tsx index af8e5ec..d1dd6da 100644 --- a/app/routes/__app.tsx +++ b/app/routes/__app.tsx @@ -2,6 +2,7 @@ import { type LinksFunction, type LoaderFunction, json } from "@remix-run/node"; import { Outlet, useCatch, useMatches } from "@remix-run/react"; import serverConfig from "~/config/config.server"; +import type { SessionData } from "~/utils/session.server"; import Footer from "~/features/core/components/footer"; import ServiceWorkerUpdateNotifier from "~/features/core/components/service-worker-update-notifier"; import Notification from "~/features/core/components/notification"; @@ -9,6 +10,7 @@ import useServiceWorkerRevalidate from "~/features/core/hooks/use-service-worker import useDevice from "~/features/phone-calls/hooks/use-device"; import footerStyles from "~/features/core/components/footer.css"; import appStyles from "~/styles/app.css"; +import { getSession } from "~/utils/session.server"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: appStyles }, @@ -16,11 +18,15 @@ export const links: LinksFunction = () => [ ]; export type AppLoaderData = { + sessionData: SessionData; config: { webPushPublicKey: string }; }; export const loader: LoaderFunction = async ({ request }) => { + const session = await getSession(request); + return json({ + sessionData: { twilio: session.data.twilio }, config: { webPushPublicKey: serverConfig.webPush.publicKey, }, diff --git a/app/routes/__app/calls.tsx b/app/routes/__app/calls.tsx index 6970abb..a096871 100644 --- a/app/routes/__app/calls.tsx +++ b/app/routes/__app/calls.tsx @@ -3,7 +3,6 @@ import { useLoaderData } from "superjson-remix"; import MissingTwilioCredentials from "~/features/core/components/missing-twilio-credentials"; import PageTitle from "~/features/core/components/page-title"; -import InactiveSubscription from "~/features/core/components/inactive-subscription"; import PhoneCallsList from "~/features/phone-calls/components/phone-calls-list"; import callsLoader, { type PhoneCallsLoaderData } from "~/features/phone-calls/loaders/calls"; import { getSeoMeta } from "~/utils/seo"; @@ -15,7 +14,7 @@ export const meta: MetaFunction = () => ({ export const loader = callsLoader; export default function PhoneCalls() { - const { hasPhoneNumber, hasOngoingSubscription } = useLoaderData(); + const { hasPhoneNumber } = useLoaderData(); if (!hasPhoneNumber) { return ( @@ -26,20 +25,6 @@ export default function PhoneCalls() { ); } - if (!hasOngoingSubscription) { - return ( - <> - -
- -
- -
-
- - ); - } - return ( <> diff --git a/app/routes/__app/keypad.tsx b/app/routes/__app/keypad.tsx index 289b183..99062c9 100644 --- a/app/routes/__app/keypad.tsx +++ b/app/routes/__app/keypad.tsx @@ -11,7 +11,6 @@ import useOnBackspacePress from "~/features/keypad/hooks/use-on-backspace-press" import Keypad from "~/features/keypad/components/keypad"; import BlurredKeypad from "~/features/keypad/components/blurred-keypad"; import MissingTwilioCredentials from "~/features/core/components/missing-twilio-credentials"; -import InactiveSubscription from "~/features/core/components/inactive-subscription"; import { getSeoMeta } from "~/utils/seo"; import { usePhoneNumber, usePressDigit, useRemoveDigit } from "~/features/keypad/hooks/atoms"; @@ -22,17 +21,13 @@ export const meta: MetaFunction = () => ({ export const loader = keypadLoader; export default function KeypadPage() { - const { hasOngoingSubscription, hasPhoneNumber, lastRecipientCalled } = useLoaderData(); + const { hasPhoneNumber, lastRecipientCalled } = useLoaderData(); const navigate = useNavigate(); const [phoneNumber, setPhoneNumber] = usePhoneNumber(); const removeDigit = useRemoveDigit(); const pressDigit = usePressDigit(); const onBackspacePress = useOnBackspacePress(); useKeyPress((key) => { - if (!hasOngoingSubscription) { - return; - } - if (key === "Backspace") { return removeDigit(); } @@ -49,15 +44,6 @@ export default function KeypadPage() { ); } - if (!hasOngoingSubscription) { - return ( - <> - - - - ); - } - return ( <>
@@ -68,7 +54,7 @@ export default function KeypadPage() {