make app usable without account, remove extra stuff
This commit is contained in:
@ -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,
|
||||
},
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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",
|
||||
};
|
@ -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");
|
||||
|
@ -1,9 +0,0 @@
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<div>
|
||||
<a className="underline" href="mailto:support@shellphone.app">
|
||||
Email us
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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",
|
||||
});
|
@ -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",
|
||||
});
|
@ -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",
|
||||
});
|
@ -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",
|
||||
});
|
@ -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 });
|
||||
};
|
@ -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);
|
||||
}),
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user