Skip to content

Commit fd01de8

Browse files
committed
The cancel form now submits the data correctly
1 parent 431eafb commit fd01de8

File tree

2 files changed

+114
-165
lines changed

2 files changed

+114
-165
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { z } from "zod";
55
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
66
import { MainBody } from "~/components/layout/AppLayout";
77
import { SideMenu } from "~/components/navigation/SideMenu";
8-
import { featuresForRequest } from "~/features.server";
98
import { useOptionalOrganization } from "~/hooks/useOrganizations";
109
import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
1110
import { useUser } from "~/hooks/useUser";

apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx

Lines changed: 114 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ const schema = z.object({
5353
type: z.enum(["free", "paid"]),
5454
planCode: z.string().optional(),
5555
callerPath: z.string(),
56-
reason: z.string().optional(),
56+
reasons: z.union([z.string(), z.array(z.string())]).optional(),
5757
message: z.string().optional(),
5858
});
5959

6060
export async function action({ request, params }: ActionFunctionArgs) {
61+
console.log("Action function called");
6162
if (request.method.toLowerCase() !== "post") {
6263
return new Response("Method not allowed", { status: 405 });
6364
}
@@ -68,13 +69,22 @@ export async function action({ request, params }: ActionFunctionArgs) {
6869

6970
const formData = await request.formData();
7071

71-
// Log the form data for debugging
72-
console.log("Form data:", Object.fromEntries(formData));
72+
console.log("All form data:", Object.fromEntries(formData));
7373

74-
const form = schema.parse(Object.fromEntries(formData));
74+
// Log the entire formData
75+
console.log("Form Data:");
76+
for (const [key, value] of formData.entries()) {
77+
console.log(`${key}: ${value}`);
78+
}
79+
80+
const reasons = formData.getAll("reasons");
81+
const message = formData.get("message");
7582

76-
// Log the parsed form data
77-
console.log("Parsed form data:", form);
83+
const form = schema.parse({
84+
...Object.fromEntries(formData),
85+
reasons: reasons.length > 1 ? reasons : reasons[0] || undefined,
86+
message: message || undefined,
87+
});
7888

7989
const organization = await prisma.organization.findUnique({
8090
where: { slug: organizationSlug },
@@ -132,12 +142,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
132142
throw redirectWithErrorMessage(form.callerPath, request, upsertCustomerRes.error.message);
133143
}
134144

135-
const formData = await request.formData();
136-
const reasons = formData.getAll("reason") as string[];
137-
const message = formData.get("message") as string | null;
138-
139145
// Only create a thread if there are reasons or a message
140-
if (reasons.length > 0 || message) {
146+
if (reasons.length > 0 || (message && message.toString().trim() !== "")) {
141147
const createThreadRes = await client.createThread({
142148
customerIdentifier: {
143149
customerId: upsertCustomerRes.data.customer.id,
@@ -157,7 +163,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
157163
text: "Reasons:",
158164
}),
159165
uiComponent.text({
160-
text: reasons.join(", "),
166+
text: Array.isArray(reasons) ? reasons.join(", ") : reasons,
161167
}),
162168
]
163169
: []),
@@ -170,7 +176,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
170176
text: "Comment:",
171177
}),
172178
uiComponent.text({
173-
text: message,
179+
text: message.toString(),
174180
}),
175181
]
176182
: []),
@@ -189,7 +195,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
189195
}
190196
}
191197
} catch (e) {
192-
console.error("Error in free case:", e);
193198
logger.error("Failed to submit to Plain the unsubscribe reason", { error: e });
194199
}
195200
payload = {
@@ -210,12 +215,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
210215
break;
211216
}
212217
default: {
213-
console.error("Invalid form type:", form.type);
214218
throw new Error("Invalid form type");
215219
}
216220
}
217221

218-
console.log("Final payload:", payload);
219222
return setPlan(organization, request, form.callerPath, payload);
220223
}
221224

@@ -262,7 +265,7 @@ type PricingPlansProps = {
262265
organizationSlug: string;
263266
hasPromotedPlan: boolean;
264267
showGithubVerificationBadge?: boolean;
265-
periodEnd: Date;
268+
periodEnd?: Date;
266269
};
267270

268271
export function PricingPlans({
@@ -281,7 +284,7 @@ export function PricingPlans({
281284
subscription={subscription}
282285
organizationSlug={organizationSlug}
283286
showGithubVerificationBadge={showGithubVerificationBadge}
284-
periodEnd={periodEnd}
287+
periodEnd={periodEnd ?? new Date()}
285288
/>
286289
<TierHobby
287290
plan={plans.hobby}
@@ -317,13 +320,15 @@ export function TierFree({
317320
const isLoading = navigation.formAction === formAction;
318321
const [isDialogOpen, setIsDialogOpen] = useState(false);
319322
const [isLackingFeaturesChecked, setIsLackingFeaturesChecked] = useState(false);
320-
321323
const status = subscription?.freeTierStatus ?? "requires_connect";
322324

323325
return (
324326
<TierContainer>
325327
<div className="relative">
326328
<PricingHeader title={plan.title} cost={0} />
329+
<TierLimit href="https://trigger.dev/pricing#computePricing">
330+
${plan.limits.includedUsage / 100} free usage
331+
</TierLimit>
327332
{showGithubVerificationBadge && status === "approved" && (
328333
<SimpleTooltip
329334
buttonClassName="absolute right-1 top-1"
@@ -377,157 +382,102 @@ export function TierFree({
377382
</div>
378383
</div>
379384
) : (
380-
<Form action={formAction} method="post" id="subscribe">
381-
<input type="hidden" name="type" value="free" />
382-
<input type="hidden" name="callerPath" value={location.pathname} />
383-
<TierLimit href="https://trigger.dev/pricing#computePricing">
384-
${plan.limits.includedUsage / 100} free usage
385-
</TierLimit>
386-
<div className="py-6">
387-
{status === "requires_connect" ? (
388-
<Dialog>
389-
<DialogTrigger asChild>
390-
<Button
391-
type="button"
392-
variant="tertiary/large"
393-
fullWidth
394-
className="text-md font-medium"
395-
disabled={isLoading}
396-
LeadingIcon={isLoading ? Spinner : undefined}
397-
>
398-
Unlock free plan
399-
</Button>
400-
</DialogTrigger>
401-
<DialogContent className="sm:max-w-md">
402-
<DialogHeader>Unlock the Free plan</DialogHeader>
403-
<div className="mb-3 mt-4 flex flex-col items-center gap-4 px-6">
404-
<GitHubLightIcon className="size-16" />
405-
<Paragraph variant="base/bright" className="text-center">
406-
To unlock the Free plan, we need to verify that you have an active GitHub
407-
account.
408-
</Paragraph>
409-
<Paragraph className="text-center">
410-
We do this to prevent malicious use of our platform. We only ask for the
411-
minimum permissions to verify your account.
385+
<>
386+
{subscription?.plan?.type !== "free" && subscription?.canceledAt === undefined ? (
387+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen} key="cancel">
388+
<DialogTrigger asChild>
389+
<Button variant="tertiary/large" fullWidth className="text-md my-6 font-medium">
390+
{`Downgrade to ${plan.title}`}
391+
</Button>
392+
</DialogTrigger>
393+
<DialogContent className="sm:max-w-md">
394+
<Form action={formAction} method="post" id="subscribe">
395+
<input type="hidden" name="type" value="free" />
396+
<input type="hidden" name="callerPath" value={location.pathname} />
397+
<DialogHeader>Downgrade plan</DialogHeader>
398+
<div className="flex items-start gap-3 pb-6 pr-4 pt-6">
399+
<ArrowDownCircleIcon className="size-12 min-w-12 text-error" />
400+
<Paragraph variant="base/bright" className="text-text-bright">
401+
Are you sure you want to downgrade? If you do, you will retain your current
402+
plan's features until <DateTime includeTime={false} date={periodEnd} />.
412403
</Paragraph>
413404
</div>
414-
<DialogFooter>
405+
<div>
406+
<div className="mb-4">
407+
<Header2 className="mb-1">Why are you thinking of downgrading?</Header2>
408+
<ul className="space-y-1">
409+
{[
410+
"Subscription or usage costs too expensive",
411+
"Bugs or technical issues",
412+
"No longer need the service",
413+
"Found a better alternative",
414+
"Lacking features I need",
415+
].map((label, index) => (
416+
<li key={index}>
417+
<CheckboxWithLabel
418+
id={`reason-${index + 1}`}
419+
name="reasons"
420+
value={label}
421+
variant="simple"
422+
label={label}
423+
labelClassName="text-text-dimmed"
424+
onChange={(isChecked: boolean) => {
425+
if (label === "Lacking features I need") {
426+
setIsLackingFeaturesChecked(isChecked);
427+
}
428+
}}
429+
/>
430+
</li>
431+
))}
432+
</ul>
433+
</div>
434+
<div>
435+
<Header2 className="mb-1">
436+
{isLackingFeaturesChecked
437+
? "What features do you need? Or how can we improve?"
438+
: "What can we do to improve?"}
439+
</Header2>
440+
<TextArea id="improvement-suggestions" name="message" />
441+
</div>
442+
</div>
443+
<DialogFooter className="mt-2">
444+
<Button variant="tertiary/medium" onClick={() => setIsDialogOpen(false)}>
445+
Dismiss
446+
</Button>
415447
<Button
416-
variant="primary/large"
417-
fullWidth
448+
variant="danger/medium"
418449
disabled={isLoading}
419-
LeadingIcon={isLoading ? Spinner : undefined}
420-
form="subscribe"
450+
LeadingIcon={
451+
isLoading && "submitting" ? () => <Spinner color="white" /> : undefined
452+
}
453+
type="submit"
421454
>
422-
Connect to GitHub
455+
Downgrade plan
423456
</Button>
424457
</DialogFooter>
425-
</DialogContent>
426-
</Dialog>
427-
) : (
428-
<>
429-
{subscription?.plan?.type !== "free" && subscription?.canceledAt === undefined ? (
430-
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen} key="cancel">
431-
<DialogTrigger asChild>
432-
<Button variant="tertiary/large" fullWidth className="text-md font-medium">
433-
{`Downgrade to ${plan.title}`}
434-
</Button>
435-
</DialogTrigger>
436-
<DialogContent className="sm:max-w-md">
437-
<DialogHeader>Downgrade plan</DialogHeader>
438-
<div className="mb-2 mt-4 flex items-start gap-3">
439-
<span>
440-
<XCircleIcon className="size-12 text-error" />
441-
</span>
442-
<Paragraph variant="base/bright" className="text-text-bright">
443-
Are you sure you want to downgrade? If you do, you will retain your
444-
current plan's features until{" "}
445-
<DateTime includeTime={false} date={periodEnd} />.
446-
</Paragraph>
447-
</div>
448-
<div>
449-
<input type="hidden" name="type" value="free" />
450-
<input type="hidden" name="callerPath" value={location.pathname} />
451-
<div className="mb-4">
452-
<Header2 className="mb-1">Why are you thinking of canceling?</Header2>
453-
<ul className="space-y-1">
454-
{[
455-
"Subscription or usage costs too expensive",
456-
"Bugs or technical issues",
457-
"No longer need the service",
458-
"Found a better alternative",
459-
"Lacking features I need",
460-
].map((label, index) => (
461-
<li key={index}>
462-
<CheckboxWithLabel
463-
id={`reason-${index + 1}`}
464-
name="reason"
465-
value={label}
466-
variant="simple"
467-
label={label}
468-
labelClassName="text-text-dimmed"
469-
onChange={(isChecked: boolean) => {
470-
if (label === "Lacking features I need") {
471-
setIsLackingFeaturesChecked(isChecked);
472-
}
473-
}}
474-
/>
475-
</li>
476-
))}
477-
</ul>
478-
</div>
479-
<div>
480-
<Header2 className="mb-1">
481-
{isLackingFeaturesChecked
482-
? "What features do you need? Or how can we improve?"
483-
: "What can we do to improve?"}
484-
</Header2>
485-
<TextArea id="improvement-suggestions" name="message" />
486-
</div>
487-
</div>
488-
<DialogFooter>
489-
<Button variant="tertiary/medium" onClick={() => setIsDialogOpen(false)}>
490-
Dismiss
491-
</Button>
492-
<Button
493-
variant="danger/medium"
494-
disabled={isLoading}
495-
LeadingIcon={
496-
isLoading && "submitting" ? () => <Spinner color="white" /> : undefined
497-
}
498-
type="submit"
499-
form="subscribe"
500-
>
501-
Downgrade plan
502-
</Button>
503-
</DialogFooter>
504-
</DialogContent>
505-
</Dialog>
506-
) : (
507-
<Button
508-
variant="tertiary/large"
509-
fullWidth
510-
className="text-md font-medium"
511-
disabled={
512-
isLoading ||
513-
subscription?.plan?.type === plan.type ||
514-
subscription?.canceledAt !== undefined
515-
}
516-
LeadingIcon={
517-
isLoading && navigation.formData?.get("planCode") === null
518-
? Spinner
519-
: undefined
520-
}
521-
>
522-
{subscription?.plan === undefined
523-
? "Select plan"
524-
: subscription.plan.type === "free" ||
525-
(subscription.canceledAt !== undefined && "Current plan")}
526-
</Button>
527-
)}
528-
</>
529-
)}
530-
</div>
458+
</Form>
459+
</DialogContent>
460+
</Dialog>
461+
) : (
462+
<Button
463+
variant="tertiary/large"
464+
fullWidth
465+
className="text-md my-6 font-medium"
466+
disabled={
467+
isLoading ||
468+
subscription?.plan?.type === plan.type ||
469+
subscription?.canceledAt !== undefined
470+
}
471+
LeadingIcon={
472+
isLoading && navigation.formData?.get("planCode") === null ? Spinner : undefined
473+
}
474+
>
475+
{subscription?.plan === undefined
476+
? "Select plan"
477+
: subscription.plan.type === "free" ||
478+
(subscription.canceledAt !== undefined && "Current plan")}
479+
</Button>
480+
)}
531481
<ul className="flex flex-col gap-2.5">
532482
<ConcurrentRuns limits={plan.limits} />
533483
<FeatureItem checked>
@@ -546,7 +496,7 @@ export function TierFree({
546496
<SupportLevel limits={plan.limits} />
547497
<Alerts limits={plan.limits} />
548498
</ul>
549-
</Form>
499+
</>
550500
)}
551501
</TierContainer>
552502
);

0 commit comments

Comments
 (0)