From e3398dda62264e15a9cf6d3b74a3646ead0039e1 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 3 Oct 2024 10:23:15 -0700 Subject: [PATCH 01/22] Checkbox component can have its label styles --- apps/webapp/app/components/primitives/Checkbox.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Checkbox.tsx b/apps/webapp/app/components/primitives/Checkbox.tsx index 20003c5e68..59f12a4048 100644 --- a/apps/webapp/app/components/primitives/Checkbox.tsx +++ b/apps/webapp/app/components/primitives/Checkbox.tsx @@ -61,6 +61,7 @@ export type CheckboxProps = Omit< description?: string; badges?: string[]; className?: string; + labelClassName?: string; onChange?: (isChecked: boolean) => void; }; @@ -78,6 +79,7 @@ export const CheckboxWithLabel = React.forwardRef e.preventDefault()} > From ac81a9cabe9bce53af69b56d1a80a46c3b0ee15c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 3 Oct 2024 10:23:46 -0700 Subject: [PATCH 02/22] Improved the dialog footer --- apps/webapp/app/components/primitives/Dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index 505b46202b..51e85ca114 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -82,7 +82,7 @@ DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
); From d8c187223e624c7a6960c3eca6da2b44c2773c34 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 3 Oct 2024 10:24:44 -0700 Subject: [PATCH 03/22] Handle sending feedback to Slack using Plain --- .../routes/resources.canceledPlanFeedback.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 apps/webapp/app/routes/resources.canceledPlanFeedback.ts diff --git a/apps/webapp/app/routes/resources.canceledPlanFeedback.ts b/apps/webapp/app/routes/resources.canceledPlanFeedback.ts new file mode 100644 index 0000000000..f14552fab3 --- /dev/null +++ b/apps/webapp/app/routes/resources.canceledPlanFeedback.ts @@ -0,0 +1,118 @@ +import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { PlainClient, uiComponent } from "@team-plain/typescript-sdk"; +import { inspect } from "util"; +import { env } from "~/env.server"; +import { redirectWithSuccessMessage } from "~/models/message.server"; +import { requireUser } from "~/services/session.server"; + +let client: PlainClient | undefined; + +export async function action({ request }: ActionFunctionArgs) { + const user = await requireUser(request); + + const formData = await request.formData(); + const path = formData.get("path") as string; + const reasons = formData.getAll("reason") as string[]; + const message = formData.get("message") as string; + + try { + if (!env.PLAIN_API_KEY) { + console.error("PLAIN_API_KEY is not set"); + return json({ error: "PLAIN_API_KEY is not set" }, { status: 500 }); + } + + client = new PlainClient({ + apiKey: env.PLAIN_API_KEY, + }); + + const upsertCustomerRes = await client.upsertCustomer({ + identifier: { + emailAddress: user.email, + }, + onCreate: { + externalId: user.id, + fullName: user.name ?? "", + email: { + email: user.email, + isVerified: true, + }, + }, + onUpdate: { + externalId: { value: user.id }, + fullName: { value: user.name ?? "" }, + email: { + email: user.email, + isVerified: true, + }, + }, + }); + + if (upsertCustomerRes.error) { + console.error( + inspect(upsertCustomerRes.error, { + showHidden: false, + depth: null, + colors: true, + }) + ); + return json({ error: upsertCustomerRes.error.message }, { status: 400 }); + } + + // Only create a thread if there are reasons or a message + if (reasons.length > 0 || message) { + const createThreadRes = await client.createThread({ + customerIdentifier: { + customerId: upsertCustomerRes.data.customer.id, + }, + title: "Plan cancelation feedback", + components: [ + uiComponent.text({ + text: `${user.name} (${user.email}) just canceled their plan.`, + }), + uiComponent.divider({ spacingSize: "M" }), + ...(reasons.length > 0 + ? [ + uiComponent.spacer({ size: "L" }), + uiComponent.text({ + size: "S", + color: "ERROR", + text: "Reasons:", + }), + uiComponent.text({ + text: reasons.join(", "), + }), + ] + : []), + ...(message + ? [ + uiComponent.spacer({ size: "L" }), + uiComponent.text({ + size: "S", + color: "ERROR", + text: "Comment:", + }), + uiComponent.text({ + text: message, + }), + ] + : []), + ], + }); + + if (createThreadRes.error) { + console.error( + inspect(createThreadRes.error, { + showHidden: false, + depth: null, + colors: true, + }) + ); + return json({ error: createThreadRes.error.message }, { status: 400 }); + } + } + + return redirectWithSuccessMessage(path, request, "Your plan has been successfully canceled."); + } catch (e) { + return json({ error: "An unexpected error occurred" }, { status: 500 }); + } +} From 22efefa788f4c158e4610df6107212cdc5406f42 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 3 Oct 2024 11:16:47 -0700 Subject: [PATCH 04/22] WIP making the modal conditional --- .../route.tsx | 1 + ...ces.orgs.$organizationSlug.select-plan.tsx | 126 +++++++++++++++--- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx index 2d37455008..c684694a5f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx @@ -117,6 +117,7 @@ export default function ChoosePlanPage() { subscription={v3Subscription} organizationSlug={organizationSlug} hasPromotedPlan={false} + periodEnd={periodEnd} // Add this line />
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index dac05d2193..51f502c482 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -4,7 +4,7 @@ import { ShieldCheckIcon, XMarkIcon, } from "@heroicons/react/20/solid"; -import { Form, useLocation, useNavigation } from "@remix-run/react"; +import { Form, useFetcher, useLocation, useNavigation } from "@remix-run/react"; import { ActionFunctionArgs } from "@remix-run/server-runtime"; import { FreePlanDefinition, @@ -34,6 +34,12 @@ import { redirectWithErrorMessage } from "~/models/message.server"; import { setPlan } from "~/services/platform.v3.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; +import { useState } from "react"; +import { XCircleIcon } from "@heroicons/react/24/outline"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Header2 } from "~/components/primitives/Headers"; +import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; +import { TextArea } from "~/components/primitives/TextArea"; const Params = z.object({ organizationSlug: z.string(), @@ -134,6 +140,7 @@ type PricingPlansProps = { organizationSlug: string; hasPromotedPlan: boolean; showGithubVerificationBadge?: boolean; + periodEnd: Date; // Add this line }; export function PricingPlans({ @@ -142,6 +149,7 @@ export function PricingPlans({ organizationSlug, hasPromotedPlan, showGithubVerificationBadge, + periodEnd, // Add this line }: PricingPlansProps) { return (
@@ -151,6 +159,7 @@ export function PricingPlans({ subscription={subscription} organizationSlug={organizationSlug} showGithubVerificationBadge={showGithubVerificationBadge} + periodEnd={periodEnd} // Add this line /> ) : ( - + <> + {subscription?.plan?.type !== "free" && subscription?.canceledAt === undefined ? ( + <> + + + + Cancel plan +
+ + + + + Are you sure you want to cancel? If you do, you will retain your current + plan's features until . + +
+
+ + +
+ Why are you thinking of canceling? +
    + {[ + "Subscription or usage costs too expensive", + "Bugs or technical issues", + "No longer need the service", + "Found a better alternative", + "Lacking features I need", + ].map((label, index) => ( +
  • + +
  • + ))} +
+
+
+ What can we do to improve? +