build pass
This commit is contained in:
parent
9f5e646338
commit
3762305c4f
@ -7,9 +7,6 @@ import listen from "test-listen";
|
|||||||
import fetch from "isomorphic-fetch";
|
import fetch from "isomorphic-fetch";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import CookieStore from "../lib/cookie-store";
|
|
||||||
import Session from "../lib/session";
|
|
||||||
|
|
||||||
type Authentication =
|
type Authentication =
|
||||||
| "none"
|
| "none"
|
||||||
| "auth0"
|
| "auth0"
|
||||||
@ -93,15 +90,13 @@ function writeSessionToCookie(
|
|||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
authentication: Authentication,
|
authentication: Authentication,
|
||||||
) {
|
) {
|
||||||
const cookieStore = new CookieStore();
|
const session = {
|
||||||
const session: Session = new Session({
|
|
||||||
id: `${authentication}|userId`,
|
id: `${authentication}|userId`,
|
||||||
email: "test@fss.test",
|
email: "test@fss.test",
|
||||||
name: "Groot",
|
name: "Groot",
|
||||||
teamId: "teamId",
|
teamId: "teamId",
|
||||||
role: "owner",
|
role: "owner",
|
||||||
});
|
};
|
||||||
cookieStore.save(req, res, session);
|
|
||||||
|
|
||||||
const setCookieHeader = res.getHeader("Set-Cookie") as string[];
|
const setCookieHeader = res.getHeader("Set-Cookie") as string[];
|
||||||
// write it to request headers to immediately have access to the user's session
|
// write it to request headers to immediately have access to the user's session
|
||||||
|
@ -1,36 +1,28 @@
|
|||||||
jest.mock("../../../../pages/api/user/_auth0", () => ({
|
|
||||||
setAppMetadata: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock("../../../../pages/api/_send-email", () => ({
|
jest.mock("../../../../pages/api/_send-email", () => ({
|
||||||
sendEmail: jest.fn(),
|
sendEmail: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock("../../../../database/users", () => ({ createUser: jest.fn() }));
|
jest.mock("../../../../database/customer", () => ({ createCustomer: jest.fn() }));
|
||||||
jest.mock("../../../../database/teams", () => ({ createTeam: jest.fn() }));
|
|
||||||
|
|
||||||
import { parse } from "set-cookie-parser";
|
import { parse } from "set-cookie-parser";
|
||||||
|
|
||||||
import { callApiHandler } from "../../../../../jest/helpers";
|
import { callApiHandler } from "../../../../../jest/helpers";
|
||||||
import signUpHandler from "../../../../pages/api/auth/sign-up";
|
import signUpHandler from "../../../../pages/api/auth/sign-up";
|
||||||
import { sessionName } from "../../../../../lib/cookie-store";
|
|
||||||
import { sendEmail } from "../../../../pages/api/_send-email";
|
import { sendEmail } from "../../../../pages/api/_send-email";
|
||||||
import { createUser } from "../../../../database/users";
|
import { createCustomer } from "../../../../database/customer";
|
||||||
import { createTeam } from "../../../../database/teams";
|
|
||||||
|
const sessionName = "";
|
||||||
|
|
||||||
describe("/api/auth/sign-up", () => {
|
describe("/api/auth/sign-up", () => {
|
||||||
const mockedSendEmail = sendEmail as jest.Mock<
|
const mockedSendEmail = sendEmail as jest.Mock<
|
||||||
ReturnType<typeof sendEmail>
|
ReturnType<typeof sendEmail>
|
||||||
>;
|
>;
|
||||||
const mockedCreateUser = createUser as jest.Mock<
|
const mockedCreateCustomer = createCustomer as jest.Mock<
|
||||||
ReturnType<typeof createUser>
|
ReturnType<typeof createCustomer>
|
||||||
>;
|
|
||||||
const mockedCreateTeam = createTeam as jest.Mock<
|
|
||||||
ReturnType<typeof createTeam>
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockedSendEmail.mockClear();
|
mockedSendEmail.mockClear();
|
||||||
mockedCreateUser.mockClear();
|
mockedCreateCustomer.mockClear();
|
||||||
mockedCreateTeam.mockClear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("responds 405 to GET", async () => {
|
test("responds 405 to GET", async () => {
|
||||||
@ -46,22 +38,11 @@ describe("/api/auth/sign-up", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("responds 200 to POST with body from email login", async () => {
|
test("responds 200 to POST with body from email login", async () => {
|
||||||
mockedCreateUser.mockResolvedValue({
|
/*mockedCreateCustomer.mockResolvedValue({
|
||||||
id: "auth0|1234567",
|
id: "auth0|1234567",
|
||||||
teamId: "98765",
|
|
||||||
role: "owner",
|
|
||||||
email: "test@fss.dev",
|
email: "test@fss.dev",
|
||||||
name: "Groot",
|
name: "Groot",
|
||||||
createdAt: new Date(),
|
});*/
|
||||||
updatedAt: new Date(),
|
|
||||||
});
|
|
||||||
mockedCreateTeam.mockResolvedValue({
|
|
||||||
id: "98765",
|
|
||||||
subscriptionId: null,
|
|
||||||
teamMembersLimit: 1,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
accessToken:
|
accessToken:
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { FunctionComponent } from "react";
|
|
||||||
import usePress from "react-gui/use-press";
|
|
||||||
|
|
||||||
const LongPressHandler: FunctionComponent = ({ children }) => {
|
|
||||||
const onLongPress = (event: any) => console.log("event", event);
|
|
||||||
const ref = usePress({ onLongPress });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} onContextMenu={e => e.preventDefault()}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LongPressHandler;
|
|
@ -1,215 +0,0 @@
|
|||||||
import type { FunctionComponent } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { CheckIcon } from "@heroicons/react/outline";
|
|
||||||
|
|
||||||
import useUser from "../../hooks/use-user";
|
|
||||||
import useSubscription from "../../hooks/use-subscription";
|
|
||||||
|
|
||||||
import type { Plan, PlanId } from "../../subscription/plans";
|
|
||||||
import {
|
|
||||||
FREE,
|
|
||||||
MONTHLY,
|
|
||||||
ANNUALLY,
|
|
||||||
TEAM_MONTHLY,
|
|
||||||
TEAM_ANNUALLY,
|
|
||||||
} from "../../subscription/plans";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
activePlanId?: PlanId;
|
|
||||||
};
|
|
||||||
const PLANS: Record<BillingSchedule, Plan[]> = {
|
|
||||||
monthly: [FREE, MONTHLY, TEAM_MONTHLY],
|
|
||||||
yearly: [FREE, ANNUALLY, TEAM_ANNUALLY],
|
|
||||||
};
|
|
||||||
|
|
||||||
const PricingPlans: FunctionComponent<Props> = ({ activePlanId }) => {
|
|
||||||
const [billingSchedule, setBillingSchedule] = useState<BillingSchedule>(
|
|
||||||
"monthly",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="sm:flex sm:flex-col sm:align-center">
|
|
||||||
<div className="relative self-center mt-6 bg-gray-100 rounded-lg p-0.5 flex sm:mt-8">
|
|
||||||
<button
|
|
||||||
onClick={() => setBillingSchedule("monthly")}
|
|
||||||
type="button"
|
|
||||||
className={clsx(
|
|
||||||
"relative w-1/2 border-gray-200 rounded-md shadow-sm py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-primary-500 focus:z-10 sm:w-auto sm:px-8",
|
|
||||||
{
|
|
||||||
"bg-white": billingSchedule === "monthly",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Monthly billing
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setBillingSchedule("yearly")}
|
|
||||||
type="button"
|
|
||||||
className={clsx(
|
|
||||||
"ml-0.5 relative w-1/2 border border-transparent rounded-md py-2 text-sm font-medium text-gray-700 whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-primary-500 focus:z-10 sm:w-auto sm:px-8",
|
|
||||||
{
|
|
||||||
"bg-white": billingSchedule === "yearly",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Yearly billing
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 space-y-4 flex flex-row flex-wrap sm:mt-10 sm:space-y-0 sm:gap-6 lg:max-w-4xl lg:mx-auto">
|
|
||||||
{PLANS[billingSchedule].map((plan) => (
|
|
||||||
<PricingPlan
|
|
||||||
key={`pricing-plan-${plan.name}`}
|
|
||||||
plan={plan}
|
|
||||||
billingSchedule={billingSchedule}
|
|
||||||
activePlanId={activePlanId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PricingPlans;
|
|
||||||
|
|
||||||
type BillingSchedule = "yearly" | "monthly";
|
|
||||||
|
|
||||||
type PricingPlanProps = {
|
|
||||||
plan: Plan;
|
|
||||||
billingSchedule: BillingSchedule;
|
|
||||||
activePlanId?: PlanId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PricingPlan: FunctionComponent<PricingPlanProps> = ({
|
|
||||||
plan,
|
|
||||||
billingSchedule,
|
|
||||||
activePlanId,
|
|
||||||
}) => {
|
|
||||||
const { subscribe, changePlan } = useSubscription();
|
|
||||||
const { userProfile } = useUser();
|
|
||||||
const { name, description, features, price, id } = plan;
|
|
||||||
const isActivePlan =
|
|
||||||
(typeof activePlanId !== "undefined" ? activePlanId : "free") === id;
|
|
||||||
|
|
||||||
function movePlan() {
|
|
||||||
const teamId = userProfile!.teamId;
|
|
||||||
const email = userProfile!.email;
|
|
||||||
const planId = plan.id;
|
|
||||||
|
|
||||||
if (typeof activePlanId === "undefined" && typeof planId === "number") {
|
|
||||||
return subscribe({ email, teamId, planId });
|
|
||||||
}
|
|
||||||
|
|
||||||
changePlan({ planId });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"bg-white w-full flex-grow border rounded-lg shadow-sm divide-y divide-gray-200 sm:w-auto",
|
|
||||||
{
|
|
||||||
"border-gray-200": !isActivePlan,
|
|
||||||
"border-primary-600": isActivePlan,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
{name}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 text-sm text-gray-500">{description}</p>
|
|
||||||
<p className="mt-8">
|
|
||||||
<PlanPrice
|
|
||||||
price={price}
|
|
||||||
billingSchedule={billingSchedule}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
<PlanButton
|
|
||||||
name={name}
|
|
||||||
isActivePlan={isActivePlan}
|
|
||||||
changePlan={movePlan}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-6 pb-8 px-6">
|
|
||||||
<h3 className="text-xs font-medium text-gray-900 tracking-wide uppercase">
|
|
||||||
What's included
|
|
||||||
</h3>
|
|
||||||
<ul className="mt-6 space-y-4">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<li
|
|
||||||
key={`pricing-plan-${name}-feature-${feature}`}
|
|
||||||
className="flex space-x-3"
|
|
||||||
>
|
|
||||||
<CheckIcon className="flex-shrink-0 h-5 w-5 text-green-500" />
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type PlanButtonProps = {
|
|
||||||
name: Plan["name"];
|
|
||||||
isActivePlan: boolean;
|
|
||||||
changePlan: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlanButton: FunctionComponent<PlanButtonProps> = ({
|
|
||||||
name,
|
|
||||||
isActivePlan,
|
|
||||||
changePlan,
|
|
||||||
}) => {
|
|
||||||
return isActivePlan ? (
|
|
||||||
<div className="block w-full py-2 text-sm font-semibold text-gray-500 text-center">
|
|
||||||
You're currently on this plan
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={changePlan}
|
|
||||||
className="block w-full cursor-pointer bg-primary-600 border border-primary-600 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-primary-700"
|
|
||||||
>
|
|
||||||
Move to <span className="font-bold">{name}</span> plan
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type PlanPriceProps = {
|
|
||||||
price: Plan["price"];
|
|
||||||
billingSchedule: BillingSchedule;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlanPrice: FunctionComponent<PlanPriceProps> = ({
|
|
||||||
price,
|
|
||||||
billingSchedule,
|
|
||||||
}) => {
|
|
||||||
if (price === "free") {
|
|
||||||
return (
|
|
||||||
<span className="text-4xl font-extrabold text-gray-900">Free</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className="text-4xl font-extrabold text-gray-900">
|
|
||||||
${price}
|
|
||||||
</span>
|
|
||||||
<span className="text-base font-medium text-gray-500">/mo</span>
|
|
||||||
{billingSchedule === "yearly" ? (
|
|
||||||
<span className="ml-1 text-sm text-gray-500">
|
|
||||||
billed yearly
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -30,7 +30,7 @@ const ProfileInformations: FunctionComponent = () => {
|
|||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("name", user.userProfile?.name ?? "");
|
setValue("name", user.userProfile?.user_metadata.name ?? "");
|
||||||
setValue("email", user.userProfile?.email ?? "");
|
setValue("email", user.userProfile?.email ?? "");
|
||||||
}, [setValue, user.userProfile]);
|
}, [setValue, user.userProfile]);
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ const ProfileInformations: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await user.updateUser({ name, email });
|
await user.updateUser({ email, data: { name } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error.response, "error updating user infos");
|
logger.error(error.response, "error updating user infos");
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { PlanId } from "../subscription/plans";
|
import type { PlanId } from "../subscription/plans";
|
||||||
import appLogger from "../../lib/logger";
|
import appLogger from "../../lib/logger";
|
||||||
|
import supabase from "../supabase/server";
|
||||||
|
|
||||||
const logger = appLogger.child({ module: "subscriptions" });
|
const logger = appLogger.child({ module: "subscriptions" });
|
||||||
|
|
||||||
@ -32,12 +33,6 @@ export type Subscription = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FirestoreSubscription = FirestoreEntry<Subscription>;
|
|
||||||
|
|
||||||
const subscriptions = firestoreCollection<FirestoreSubscription>(
|
|
||||||
"subscriptions",
|
|
||||||
);
|
|
||||||
|
|
||||||
type CreateSubscriptionParams = Pick<
|
type CreateSubscriptionParams = Pick<
|
||||||
Subscription,
|
Subscription,
|
||||||
| "userId"
|
| "userId"
|
||||||
@ -62,24 +57,25 @@ export async function createSubscription({
|
|||||||
cancelUrl,
|
cancelUrl,
|
||||||
lastEventTime,
|
lastEventTime,
|
||||||
}: CreateSubscriptionParams): Promise<Subscription> {
|
}: CreateSubscriptionParams): Promise<Subscription> {
|
||||||
const createdAt = FieldValue.serverTimestamp() as Timestamp;
|
const createdAt = new Date();
|
||||||
await subscriptions.doc(paddleSubscriptionId).set({
|
const { data } = await supabase
|
||||||
userId,
|
.from<Subscription>("subscription")
|
||||||
planId,
|
.insert({
|
||||||
paddleCheckoutId,
|
userId,
|
||||||
paddleSubscriptionId,
|
planId,
|
||||||
nextBillDate,
|
paddleCheckoutId,
|
||||||
status,
|
paddleSubscriptionId,
|
||||||
updateUrl,
|
nextBillDate,
|
||||||
cancelUrl,
|
status,
|
||||||
lastEventTime,
|
updateUrl,
|
||||||
createdAt,
|
cancelUrl,
|
||||||
updatedAt: createdAt,
|
lastEventTime,
|
||||||
});
|
createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
})
|
||||||
|
.throwOnError();
|
||||||
|
|
||||||
const subscription = await findSubscription({ paddleSubscriptionId });
|
return data![0];
|
||||||
|
|
||||||
return subscription!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetSubscriptionParams = Pick<Subscription, "paddleSubscriptionId">;
|
type GetSubscriptionParams = Pick<Subscription, "paddleSubscriptionId">;
|
||||||
@ -87,14 +83,15 @@ type GetSubscriptionParams = Pick<Subscription, "paddleSubscriptionId">;
|
|||||||
export async function findSubscription({
|
export async function findSubscription({
|
||||||
paddleSubscriptionId,
|
paddleSubscriptionId,
|
||||||
}: GetSubscriptionParams): Promise<Subscription | undefined> {
|
}: GetSubscriptionParams): Promise<Subscription | undefined> {
|
||||||
const subscriptionDocument = await subscriptions
|
const { error, data } = await supabase
|
||||||
.doc(paddleSubscriptionId)
|
.from<Subscription>("subscription")
|
||||||
.get();
|
.select("*")
|
||||||
if (!subscriptionDocument.exists) {
|
.eq("paddleSubscriptionId", paddleSubscriptionId)
|
||||||
return;
|
.single();
|
||||||
}
|
|
||||||
|
|
||||||
return convertFromFirestore(subscriptionDocument.data()!);
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindUserSubscriptionParams = Pick<Subscription, "userId">;
|
type FindUserSubscriptionParams = Pick<Subscription, "userId">;
|
||||||
@ -102,18 +99,16 @@ type FindUserSubscriptionParams = Pick<Subscription, "userId">;
|
|||||||
export async function findUserSubscription({
|
export async function findUserSubscription({
|
||||||
userId,
|
userId,
|
||||||
}: FindUserSubscriptionParams): Promise<Subscription | null> {
|
}: FindUserSubscriptionParams): Promise<Subscription | null> {
|
||||||
const subscriptionDocumentsSnapshot = await subscriptions
|
const { error, data } = await supabase
|
||||||
.where("userId", "==", userId)
|
.from<Subscription>("subscription")
|
||||||
.where("status", "!=", "deleted")
|
.select("*")
|
||||||
.get();
|
.eq("userId", userId)
|
||||||
if (subscriptionDocumentsSnapshot.docs.length === 0) {
|
.neq("status", "deleted")
|
||||||
logger.warn(`No subscription found for user ${userId}`);
|
.single();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscriptionDocument = subscriptionDocumentsSnapshot.docs[0].data();
|
if (error) throw error;
|
||||||
|
|
||||||
return convertFromFirestore(subscriptionDocument);
|
return data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSubscriptionParams = Pick<Subscription, "paddleSubscriptionId"> &
|
type UpdateSubscriptionParams = Pick<Subscription, "paddleSubscriptionId"> &
|
||||||
@ -135,17 +130,22 @@ export async function updateSubscription(
|
|||||||
update: UpdateSubscriptionParams,
|
update: UpdateSubscriptionParams,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const paddleSubscriptionId = update.paddleSubscriptionId;
|
const paddleSubscriptionId = update.paddleSubscriptionId;
|
||||||
await subscriptions.doc(paddleSubscriptionId).set(
|
await supabase
|
||||||
{
|
.from<Subscription>("subscription")
|
||||||
|
.update({
|
||||||
...update,
|
...update,
|
||||||
updatedAt: FieldValue.serverTimestamp() as Timestamp,
|
updatedAt: new Date(),
|
||||||
},
|
})
|
||||||
{ merge: true },
|
.eq("paddleSubscriptionId", paddleSubscriptionId)
|
||||||
);
|
.throwOnError();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSubscription({
|
export async function deleteSubscription({
|
||||||
paddleSubscriptionId,
|
paddleSubscriptionId,
|
||||||
}: Pick<Subscription, "paddleSubscriptionId">): Promise<void> {
|
}: Pick<Subscription, "paddleSubscriptionId">): Promise<void> {
|
||||||
await subscriptions.doc(paddleSubscriptionId).delete();
|
await supabase
|
||||||
|
.from<Subscription>("subscription")
|
||||||
|
.delete()
|
||||||
|
.eq("paddleSubscriptionId", paddleSubscriptionId)
|
||||||
|
.throwOnError();
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
SUBSCRIPTION_STATUSES,
|
SUBSCRIPTION_STATUSES,
|
||||||
updateSubscription,
|
updateSubscription,
|
||||||
} from "../../../database/subscriptions";
|
} from "../../../database/subscriptions";
|
||||||
import { FREE } from "../../../subscription/plans";
|
|
||||||
import appLogger from "../../../../lib/logger";
|
import appLogger from "../../../../lib/logger";
|
||||||
|
|
||||||
const logger = appLogger.child({ module: "subscription-cancelled" });
|
const logger = appLogger.child({ module: "subscription-cancelled" });
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { sendEmail } from "../_send-email";
|
import { sendEmail } from "../_send-email";
|
||||||
import type { ApiError } from "../_types";
|
import type { ApiError } from "../_types";
|
||||||
import appLogger from "../../../../lib/logger";
|
import appLogger from "../../../../lib/logger";
|
||||||
import { PAID_PLANS } from "../../../subscription/plans";
|
import { PLANS } from "../../../subscription/plans";
|
||||||
|
|
||||||
const logger = appLogger.child({ module: "subscription-created" });
|
const logger = appLogger.child({ module: "subscription-created" });
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ export async function subscriptionCreatedHandler(
|
|||||||
cancelUrl,
|
cancelUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextPlan = PAID_PLANS[planId];
|
const nextPlan = PLANS[planId];
|
||||||
sendEmail({
|
sendEmail({
|
||||||
subject: "Thanks for your purchase",
|
subject: "Thanks for your purchase",
|
||||||
body: `Welcome to ${nextPlan.name} plan`,
|
body: `Welcome to ${nextPlan.name} plan`,
|
||||||
|
@ -8,9 +8,10 @@ import {
|
|||||||
SUBSCRIPTION_STATUSES,
|
SUBSCRIPTION_STATUSES,
|
||||||
updateSubscription,
|
updateSubscription,
|
||||||
} from "../../../database/subscriptions";
|
} from "../../../database/subscriptions";
|
||||||
import { PAID_PLANS } from "../../../subscription/plans";
|
import { PLANS } from "../../../subscription/plans";
|
||||||
import appLogger from "../../../../lib/logger";
|
import appLogger from "../../../../lib/logger";
|
||||||
import { sendEmail } from "../_send-email";
|
import { sendEmail } from "../_send-email";
|
||||||
|
import { findCustomer } from "../../../database/customer";
|
||||||
|
|
||||||
const logger = appLogger.child({ module: "subscription-updated" });
|
const logger = appLogger.child({ module: "subscription-updated" });
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ export async function subscriptionUpdated(
|
|||||||
const updateUrl = body.update_url;
|
const updateUrl = body.update_url;
|
||||||
const cancelUrl = body.cancel_url;
|
const cancelUrl = body.cancel_url;
|
||||||
const planId = body.subscription_plan_id;
|
const planId = body.subscription_plan_id;
|
||||||
const nextPlan = PAID_PLANS[planId];
|
const nextPlan = PLANS[planId];
|
||||||
await updateSubscription({
|
await updateSubscription({
|
||||||
paddleSubscriptionId,
|
paddleSubscriptionId,
|
||||||
planId,
|
planId,
|
||||||
@ -79,7 +80,7 @@ export async function subscriptionUpdated(
|
|||||||
cancelUrl,
|
cancelUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await findUser({ id: subscription.userId });
|
const user = await findCustomer(subscription.userId);
|
||||||
|
|
||||||
sendEmail({
|
sendEmail({
|
||||||
subject: "Thanks for your purchase",
|
subject: "Thanks for your purchase",
|
||||||
|
@ -24,7 +24,7 @@ const bodySchema = Joi.object<Body>({
|
|||||||
export default withApiAuthRequired<Response>(async function updateSubscription(
|
export default withApiAuthRequired<Response>(async function updateSubscription(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
session,
|
user,
|
||||||
) {
|
) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
const statusCode = 405;
|
const statusCode = 405;
|
||||||
@ -57,7 +57,7 @@ export default withApiAuthRequired<Response>(async function updateSubscription(
|
|||||||
const { planId }: Body = validationResult.value;
|
const { planId }: Body = validationResult.value;
|
||||||
|
|
||||||
const subscription = await findUserSubscription({
|
const subscription = await findUserSubscription({
|
||||||
teamId: session.user.teamId,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
const statusCode = 500;
|
const statusCode = 500;
|
||||||
|
@ -4,6 +4,7 @@ import { deleteSubscription } from "../../../database/subscriptions";
|
|||||||
import { cancelPaddleSubscription } from "../../../subscription/_paddle-api";
|
import { cancelPaddleSubscription } from "../../../subscription/_paddle-api";
|
||||||
|
|
||||||
import appLogger from "../../../../lib/logger";
|
import appLogger from "../../../../lib/logger";
|
||||||
|
import supabase from "../../../supabase/server";
|
||||||
|
|
||||||
type Response = void | ApiError;
|
type Response = void | ApiError;
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ const logger = appLogger.child({ route: "/api/user/delete-user" });
|
|||||||
export default withApiAuthRequired<Response>(async function deleteUserHandler(
|
export default withApiAuthRequired<Response>(async function deleteUserHandler(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
session,
|
user,
|
||||||
) {
|
) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
const statusCode = 405;
|
const statusCode = 405;
|
||||||
@ -27,34 +28,12 @@ export default withApiAuthRequired<Response>(async function deleteUserHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id: userId, role, teamId } = session.user;
|
|
||||||
const team = await findTeam({ id: teamId });
|
|
||||||
const subscriptionId = team!.subscriptionId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let actions: Promise<any>[] = [
|
let actions: Promise<any>[] = [
|
||||||
deleteAuth0User({ id: userId }),
|
supabase.auth.api.deleteUser(user.id, ""),
|
||||||
deleteUser({ id: userId, teamId }),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (role === "owner") {
|
// TODO: delete user phone number/messages
|
||||||
const teamMembers = await findUsersByTeam({ teamId });
|
|
||||||
|
|
||||||
teamMembers.forEach((member) =>
|
|
||||||
actions.push(deleteUser({ id: member.id, teamId })),
|
|
||||||
);
|
|
||||||
actions.push(deleteTeam({ id: teamId }));
|
|
||||||
|
|
||||||
if (subscriptionId) {
|
|
||||||
actions.push(
|
|
||||||
cancelPaddleSubscription({ subscriptionId }),
|
|
||||||
deleteSubscription({
|
|
||||||
paddleSubscriptionId: subscriptionId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(actions);
|
await Promise.all(actions);
|
||||||
|
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import type { ApiError } from "../_types";
|
|
||||||
import type Session from "../../../../lib/session";
|
|
||||||
import {
|
|
||||||
sessionCache,
|
|
||||||
withApiAuthRequired,
|
|
||||||
} from "../../../../lib/session-helpers";
|
|
||||||
import appLogger from "../../../../lib/logger";
|
|
||||||
|
|
||||||
type Response = Session | ApiError;
|
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/user/session" });
|
|
||||||
|
|
||||||
export default withApiAuthRequired<Response>(async function session(req, res) {
|
|
||||||
if (req.method !== "GET") {
|
|
||||||
const statusCode = 405;
|
|
||||||
const apiError: ApiError = {
|
|
||||||
statusCode,
|
|
||||||
errorMessage: `Method ${req.method} Not Allowed`,
|
|
||||||
};
|
|
||||||
logger.error(apiError);
|
|
||||||
|
|
||||||
res.setHeader("Allow", ["GET"]);
|
|
||||||
res.status(statusCode).send(apiError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = sessionCache.get(req, res)!;
|
|
||||||
|
|
||||||
res.status(200).send(session);
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user