Skip to content

Commit 7e56062

Browse files
claude[bot]Trigger.dev RepoOps
authored andcommitted
feat(webapp): randomise the order of downgrade feedback options
**Before:** the "Why are you thinking of downgrading?" dialog always listed its six reasons in the same fixed order, so options near the top were more likely to be picked than the ones at the bottom. **After:** the same six reasons appear in a random order, reshuffled on each page load. The free-text box below the list is unchanged. How: the list is lifted to a module-level constant and shuffled once per mount with a Fisher-Yates shuffle over a copy, through a `useState` initialiser so ticking a checkbox does not reorder the list mid-interaction. Answers are submitted by value rather than by position, so the reordering does not change the submitted payload; the checkbox ids now derive from the label instead of the index. Mono-RevId: 79ad55c0c74576161864d85f1d1eb07891499ede
1 parent f3922e6 commit 7e56062

1 file changed

Lines changed: 32 additions & 11 deletions

File tree

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,32 @@ export function PricingPlans({
313313
);
314314
}
315315

316+
const LACKING_FEATURES_REASON = "Lacking features I need";
317+
318+
const DOWNGRADE_REASONS = [
319+
"The Free plan is all I need",
320+
"Subscription or usage costs too expensive",
321+
"Bugs or technical issues",
322+
"No longer need the service",
323+
"Found a better alternative",
324+
LACKING_FEATURES_REASON,
325+
] as const;
326+
327+
/** Unbiased Fisher-Yates on a copy, so the source array is never mutated. */
328+
function shuffleArray<T>(items: readonly T[]): T[] {
329+
const shuffled = [...items];
330+
for (let i = shuffled.length - 1; i > 0; i--) {
331+
const j = Math.floor(Math.random() * (i + 1));
332+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
333+
}
334+
return shuffled;
335+
}
336+
337+
/** A stable id per reason, so ids don't move when the list order does. */
338+
function reasonId(reason: string) {
339+
return `reason-${reason.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
340+
}
341+
316342
export function TierFree({
317343
plan,
318344
subscription,
@@ -330,6 +356,8 @@ export function TierFree({
330356
const isLoading = navigation.formAction === formAction;
331357
const [isDialogOpen, setIsDialogOpen] = useState(false);
332358
const [isLackingFeaturesChecked, setIsLackingFeaturesChecked] = useState(false);
359+
// Randomised once per mount so list position doesn't bias which reasons get picked.
360+
const [shuffledReasons] = useState(() => shuffleArray(DOWNGRADE_REASONS));
333361

334362
useEffect(() => {
335363
// oxlint-disable-next-line react/set-state-in-effect -- This effect intentionally synchronizes route state after an external or lifecycle change.
@@ -367,24 +395,17 @@ export function TierFree({
367395
<div className="mb-4">
368396
<Header2 className="mb-1">Why are you thinking of downgrading?</Header2>
369397
<ul className="space-y-1">
370-
{[
371-
"The Free plan is all I need",
372-
"Subscription or usage costs too expensive",
373-
"Bugs or technical issues",
374-
"No longer need the service",
375-
"Found a better alternative",
376-
"Lacking features I need",
377-
].map((label, index) => (
378-
<li key={index}>
398+
{shuffledReasons.map((label) => (
399+
<li key={label}>
379400
<CheckboxWithLabel
380-
id={`reason-${index + 1}`}
401+
id={reasonId(label)}
381402
name="reasons"
382403
value={label}
383404
variant="simple"
384405
label={label}
385406
labelClassName="text-text-dimmed"
386407
onChange={(isChecked: boolean) => {
387-
if (label === "Lacking features I need") {
408+
if (label === LACKING_FEATURES_REASON) {
388409
setIsLackingFeaturesChecked(isChecked);
389410
}
390411
}}

0 commit comments

Comments
 (0)