SIGN IN SIGN UP

AutoGPT is the vision of accessible AI for everyone, to use and to build on. Our mission is to provide the tools, so that you can focus on what matters.

0 0 79 Python

feat(frontend): smooth Monthly↔Yearly price transition on PlanCard (#13100)

### Why / What / How

**Why:** When toggling Monthly ↔ Yearly on the onboarding subscription
step and the paywall modal, the price values snap instantly and the
"Save 15%" row pops in/out — producing the "slight weird flash" called
out in [SECRT-2347](https://linear.app/autogpt/issue/SECRT-2347). The
card itself doesn't need to morph; only the numbers and the savings row
do.

**What:** Subtle in-place animations on `PlanCard` numbers so the cycle
switch feels closer to Linear / Figma:

- `$50.00 → $42.50` (primary price): crossfades in place with a 6px
y-shift + 2px blur + opacity (220ms ease-out). No layout change, no
width jump on plans where digits change.
- `Charged today: $510.00 → $50.00`: same crossfade; kept as a single
text node so existing `getByText("Charged today: $X")` assertions still
match.
- `Save 15%` + struck-through original price row: height-collapse + fade
+ blur (240ms ease-out) instead of mounting/unmounting abruptly.

**How:**

- New `AnimatedAmount` component in
`src/components/molecules/PlanCard/AnimatedAmount.tsx`. Uses
`<AnimatePresence mode="popLayout">` keyed on the formatted string so
each value change triggers an enter+exit, with the exiting element taken
out of flow (no double-width spacer). Falls back to a plain span when
`useReducedMotion()` is true.
- `PlanCard.tsx` swaps the three changing amounts for `<AnimatedAmount>`
and wraps the savings row in `<AnimatePresence>` with `height: 0 ↔
"auto"`, opacity, and a small blur bridge.
- Easing: `cubic-bezier(0.22, 1, 0.36, 1)` (ease-out); durations
220–240ms — per Emil Kowalski's UI animation guidelines
(transform/opacity/filter only, <300ms, blur to bridge state changes).


https://github.com/user-attachments/assets/83b35c19-1c77-49b3-bacd-0b7a282e7b76

### Changes 🏗️

- Add
`autogpt_platform/frontend/src/components/molecules/PlanCard/AnimatedAmount.tsx`
— reusable keyed crossfade primitive (`AnimatePresence` + `popLayout`,
respects `prefers-reduced-motion`).
- Update
`autogpt_platform/frontend/src/components/molecules/PlanCard/PlanCard.tsx`:
- Wrap primary price, monthly-original (line-through), and "Charged
today" amounts in `<AnimatedAmount>`.
- Wrap the "Save X%" + line-through row in `<AnimatePresence>` with
height/opacity/blur exit so it eases in/out instead of popping.

### Checklist 📋

#### For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Onboarding → subscription step loads on **Yearly** by default;
price reads `$42.50 / month` for Pro, `$272.00 / month` for Max; the
"Save 15%" row is visible.
- [x] Click **Monthly billing** → Pro and Max prices ease in place to
`$50.00` / `$320.00`; "Save 15%" row collapses smoothly; "Charged today"
lines crossfade to the matching monthly amount. No flash, no layout jump
on the rest of the card.
- [x] Click **Yearly billing** → reverses cleanly; "Save 15%" eases back
in.
- [x] Repeated toggling rapidly does not visibly jitter (AnimatePresence
interrupts cleanly).
- [x] Paywall modal (`PaywallModal`) — same toggle exercises the same
`PlanCard` and animates the same way.
- [x] System "Reduce motion" enabled (macOS Accessibility) → values swap
without animation; no errors.
- [x] Existing integration tests `SubscriptionStep.test.tsx` and
`PaywallModal.test.tsx` still find prices by `getByText("$X.XX")` and
`getByText("Charged today: $X.XX")` because each amount remains a single
text node in steady state.

#### For configuration changes:

- [x] `.env.default` is updated or already compatible with my changes
- [x] `docker-compose.yml` is updated or already compatible with my
changes
- [x] I have included a list of my configuration changes in the PR
description (under **Changes**)
A
Abhimanyu Yadav committed
cee87967e7eed3108bcd66b5d033a586934c8f3f
Parent: 16f94ed
Committed by GitHub <noreply@github.com> on 5/13/2026, 8:24:47 AM