make app usable without account, remove extra stuff

This commit is contained in:
m5r
2023-04-29 18:30:07 +02:00
parent cb35455722
commit 03ae466c66
128 changed files with 617 additions and 14061 deletions

View File

@ -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<AppLoaderData>({
sessionData: { twilio: session.data.twilio },
config: {
webPushPublicKey: serverConfig.webPush.publicKey,
},

View File

@ -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<PhoneCallsLoaderData>();
const { hasPhoneNumber } = useLoaderData<PhoneCallsLoaderData>();
if (!hasPhoneNumber) {
return (
@ -26,20 +25,6 @@ export default function PhoneCalls() {
);
}
if (!hasOngoingSubscription) {
return (
<>
<InactiveSubscription />
<div className="filter blur-sm select-none absolute top-0 w-full h-full z-0">
<PageTitle title="Calls" />
<section className="relative flex flex-grow flex-col">
<PhoneCallsList />
</section>
</div>
</>
);
}
return (
<>
<PageTitle className="pl-12" title="Calls" />

View File

@ -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<KeypadLoaderData>();
const { hasPhoneNumber, lastRecipientCalled } = useLoaderData<KeypadLoaderData>();
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 (
<>
<InactiveSubscription />
<BlurredKeypad />
</>
);
}
return (
<>
<div className="w-96 h-full flex flex-col justify-around py-5 mx-auto text-center text-black">
@ -68,7 +54,7 @@ export default function KeypadPage() {
<Keypad>
<button
onClick={async () => {
if (!hasPhoneNumber || !hasOngoingSubscription) {
if (!hasPhoneNumber) {
return;
}

View File

@ -4,7 +4,6 @@ import clsx from "clsx";
import {
IoLogOutOutline,
IoNotificationsOutline,
IoCardOutline,
IoCallOutline,
IoPersonCircleOutline,
IoHelpBuoyOutline,
@ -14,11 +13,8 @@ import Divider from "~/features/settings/components/divider";
import { getSeoMeta } from "~/utils/seo";
const subNavigation = [
{ name: "Account", to: "/settings/account", icon: IoPersonCircleOutline },
{ name: "Phone", to: "/settings/phone", icon: IoCallOutline },
{ name: "Billing", to: "/settings/billing", icon: IoCardOutline },
{ name: "Notifications", to: "/settings/notifications", icon: IoNotificationsOutline },
{ name: "Support", to: "/settings/support", icon: IoHelpBuoyOutline },
];
export const meta: MetaFunction = () => ({
@ -62,15 +58,6 @@ export default function SettingsLayout() {
)}
</NavLink>
))}
<Divider />
<Link
to="/sign-out"
className="group text-gray-900 hover:text-gray-900 hover:bg-gray-50 rounded-md px-3 py-2 flex items-center text-sm font-medium"
>
<IoLogOutOutline className="text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" />
Log out
</Link>
</nav>
</aside>

View File

@ -1,18 +0,0 @@
import accountAction from "~/features/settings/actions/account";
import ProfileInformations from "~/features/settings/components/account/profile-informations";
import UpdatePassword from "~/features/settings/components/account/update-password";
import DangerZone from "~/features/settings/components/account/danger-zone";
export const action = accountAction;
export default function Account() {
return (
<div className="flex flex-col space-y-6">
<ProfileInformations />
<UpdatePassword />
<DangerZone />
</div>
);
}

View File

@ -1,66 +0,0 @@
import { SubscriptionStatus } from "@prisma/client";
import usePaymentsHistory from "~/features/settings/hooks/use-payments-history";
import SettingsSection from "~/features/settings/components/settings-section";
import BillingHistory from "~/features/settings/components/billing/billing-history";
import Divider from "~/features/settings/components/divider";
import Plans from "~/features/settings/components/billing/plans";
import PaddleLink from "~/features/settings/components/billing/paddle-link";
function useSubscription() {
return {
subscription: null as any,
cancelSubscription: () => void 0,
updatePaymentMethod: () => void 0,
};
}
export default function Billing() {
const { count: paymentsCount } = usePaymentsHistory();
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription();
return (
<>
{subscription ? (
<SettingsSection>
{subscription.status === SubscriptionStatus.deleted ? (
<p>
Your {plansName[subscription.paddlePlanId]?.toLowerCase()} subscription is cancelled and
will expire on {subscription.cancellationEffectiveDate!.toLocaleDateString()}.
</p>
) : (
<>
<p>Current plan: {subscription.paddlePlanId}</p>
<PaddleLink
onClick={() => updatePaymentMethod(/*{ updateUrl: subscription.updateUrl }*/)}
text="Update payment method"
/>
<PaddleLink
onClick={() => cancelSubscription(/*{ cancelUrl: subscription.cancelUrl }*/)}
text="Cancel subscription"
/>
</>
)}
</SettingsSection>
) : null}
{paymentsCount > 0 ? (
<>
<BillingHistory />
<div className="hidden lg:block lg:py-3">
<Divider />
</div>
</>
) : null}
<Plans />
<p className="text-sm text-gray-500">Prices include all applicable sales taxes.</p>
</>
);
}
const plansName: Record<number, string> = {
727544: "Yearly",
727540: "Monthly",
};

View File

@ -1,4 +1,4 @@
import type { LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
export const loader: LoaderFunction = () => redirect("/settings/account");
export const loader: LoaderFunction = () => redirect("/settings/phone");

View File

@ -1,9 +0,0 @@
export default function SupportPage() {
return (
<div>
<a className="underline" href="mailto:support@shellphone.app">
Email us
</a>
</div>
);
}

View File

@ -1,16 +0,0 @@
import { Link, Outlet } from "@remix-run/react";
import Logo from "~/features/core/components/logo";
export default function AuthLayout() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 px-8">
<div className="mx-auto">
<Link to="/" prefetch="intent">
<Logo className="mx-auto w-16" />
</Link>
</div>
<Outlet />
</div>
);
}

View File

@ -1,15 +0,0 @@
import type { MetaFunction } from "@remix-run/node";
import ForgotPasswordPage from "~/features/auth/pages/forgot-password";
import forgotPasswordAction from "~/features/auth/actions/forgot-password";
import forgotPasswordLoader from "~/features/auth/loaders/forgot-password";
import { getSeoMeta } from "~/utils/seo";
export default ForgotPasswordPage;
export const action = forgotPasswordAction;
export const loader = forgotPasswordLoader;
export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "Forgot password" }),
robots: "noindex",
googlebot: "noindex",
});

View File

@ -1,15 +0,0 @@
import type { MetaFunction } from "@remix-run/node";
import RegisterPage from "~/features/auth/pages/register";
import registerAction from "~/features/auth/actions/register";
import registerLoader from "~/features/auth/loaders/register";
import { getSeoMeta } from "~/utils/seo";
export default RegisterPage;
export const action = registerAction;
export const loader = registerLoader;
export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "Register" }),
robots: "noindex",
googlebot: "noindex",
});

View File

@ -1,15 +0,0 @@
import type { MetaFunction } from "@remix-run/node";
import ResetPasswordPage from "~/features/auth/pages/reset-password";
import resetPasswordAction from "~/features/auth/actions/reset-password";
import resetPasswordLoader from "~/features/auth/loaders/reset-password";
import { getSeoMeta } from "~/utils/seo";
export default ResetPasswordPage;
export const action = resetPasswordAction;
export const loader = resetPasswordLoader;
export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "Reset password" }),
robots: "noindex",
googlebot: "noindex",
});

View File

@ -1,15 +0,0 @@
import type { MetaFunction } from "@remix-run/node";
import SignInPage from "~/features/auth/pages/sign-in";
import signInAction from "~/features/auth/actions/sign-in";
import signInLoader from "~/features/auth/loaders/sign-in";
import { getSeoMeta } from "~/utils/seo";
export default SignInPage;
export const action = signInAction;
export const loader = signInLoader;
export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "Sign in" }),
robots: "noindex",
googlebot: "noindex",
});

View File

@ -1,9 +0,0 @@
import type { LoaderFunction } from "@remix-run/node";
import authenticator from "~/utils/authenticator.server";
export const loader: LoaderFunction = async ({ request }) => {
const searchParams = new URL(request.url).searchParams;
const redirectTo = searchParams.get("redirectTo") ?? "/";
await authenticator.logout(request, { redirectTo });
};

View File

@ -26,7 +26,7 @@ export const loader: LoaderFunction = async ({ request }) => {
throw new Error(`Queue "${queueName}"'s scheduler is not running`);
}
}),
db.user.count(),
db.twilioAccount.count(),
fetch(url.toString(), { method: "HEAD" }).then((r) => {
if (!r.ok) return Promise.reject(r);
}),

View File

@ -1,17 +1,5 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import { type LoaderArgs, redirect } from "@remix-run/node";
import joinWaitlistAction from "~/features/public-area/actions/index";
import IndexPage from "~/features/public-area/pages/index";
import { getSeoMeta } from "~/utils/seo";
import styles from "../styles/index.css";
export const action = joinWaitlistAction;
export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "", description: "Welcome to Remixtape!" }),
});
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
export default IndexPage;
export async function loader({ request }: LoaderArgs) {
return redirect("/messages");
}

View File

@ -1,7 +1,7 @@
import { type ActionFunction } from "@remix-run/node";
import { badRequest, serverError } from "remix-utils";
import { z } from "zod";
import { Direction, Prisma, SubscriptionStatus } from "@prisma/client";
import { Direction, Prisma } from "@prisma/client";
import logger from "~/utils/logger.server";
import db from "~/utils/db.server";
@ -42,29 +42,7 @@ async function handleIncomingCall(formData: unknown, twilioSignature: string) {
twilioAccountSid: body.AccountSid,
},
include: {
twilioAccount: {
include: {
organization: {
select: {
subscriptions: {
where: {
OR: [
{ status: { not: SubscriptionStatus.deleted } },
{
status: SubscriptionStatus.deleted,
cancellationEffectiveDate: { gt: new Date() },
},
],
},
orderBy: { lastEventTime: Prisma.SortOrder.desc },
},
memberships: {
select: { user: true },
},
},
},
},
},
twilioAccount: true,
},
});
if (!phoneNumber) {
@ -72,13 +50,6 @@ async function handleIncomingCall(formData: unknown, twilioSignature: string) {
return new Response(null, { status: 402 });
}
if (phoneNumber.twilioAccount.organization.subscriptions.length === 0) {
// decline the outgoing call because
// the organization is on the free plan
console.log("no active subscription"); // TODO: uncomment the line below
// return new Response(null, { status: 402 });
}
const encryptedAuthToken = phoneNumber.twilioAccount.authToken;
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
if (!phoneNumber || !encryptedAuthToken || !twilio.validateRequest(authToken, twilioSignature, voiceUrl, body)) {
@ -99,8 +70,7 @@ async function handleIncomingCall(formData: unknown, twilioSignature: string) {
});
// await notify(); TODO
const user = phoneNumber.twilioAccount.organization.memberships[0].user!;
const identity = `${phoneNumber.twilioAccount.accountSid}__${user.id}`;
const identity = `shellphone__${phoneNumber.twilioAccount.accountSid}`;
const voiceResponse = new twilio.twiml.VoiceResponse();
const dial = voiceResponse.dial({ answerOnBridge: true });
dial.client(identity); // TODO: si le device n'est pas registered => call failed *shrug*
@ -118,32 +88,15 @@ async function handleOutgoingCall(formData: unknown, twilioSignature: string) {
const body = validation.data;
const recipient = body.To;
const accountSid = body.From.slice("client:".length).split("__")[0];
const accountSid = body.From.slice("client:".length).split("__")[1];
try {
const twilioAccount = await db.twilioAccount.findUnique({
where: { accountSid },
include: {
organization: {
select: {
subscriptions: {
where: {
OR: [
{ status: { not: SubscriptionStatus.deleted } },
{
status: SubscriptionStatus.deleted,
cancellationEffectiveDate: { gt: new Date() },
},
],
},
orderBy: { lastEventTime: Prisma.SortOrder.desc },
},
},
},
},
});
if (!twilioAccount) {
// this shouldn't be happening
logger.warn("this shouldn't be happening, no twilio account found");
return new Response(null, { status: 402 });
}
@ -152,16 +105,12 @@ async function handleOutgoingCall(formData: unknown, twilioSignature: string) {
});
if (!phoneNumber) {
// this shouldn't be happening
logger.warn(
`this shouldn't be happening, no phone number found for twilio account ${twilioAccount.accountSid}`,
);
return new Response(null, { status: 402 });
}
if (twilioAccount.organization.subscriptions.length === 0) {
// decline the outgoing call because
// the organization is on the free plan
console.log("no active subscription"); // TODO: uncomment the line below
// return new Response(null, { status: 402 });
}
const encryptedAuthToken = twilioAccount.authToken;
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
if (

View File

@ -2,7 +2,6 @@ import { type ActionFunction } from "@remix-run/node";
import { badRequest, html, notFound, serverError } from "remix-utils";
import twilio from "twilio";
import { z } from "zod";
import { Prisma, SubscriptionStatus } from "@prisma/client";
import insertIncomingMessageQueue from "~/queues/insert-incoming-message.server";
import notifyIncomingMessageQueue from "~/queues/notify-incoming-message.server";
@ -30,26 +29,7 @@ export const action: ActionFunction = async ({ request }) => {
const phoneNumbers = await db.phoneNumber.findMany({
where: { number: body.To },
include: {
twilioAccount: {
include: {
organization: {
select: {
subscriptions: {
where: {
OR: [
{ status: { not: SubscriptionStatus.deleted } },
{
status: SubscriptionStatus.deleted,
cancellationEffectiveDate: { gt: new Date() },
},
],
},
orderBy: { lastEventTime: Prisma.SortOrder.desc },
},
},
},
},
},
twilioAccount: true,
},
});
if (phoneNumbers.length === 0) {