Files
overleaf-cep/services/web/frontend/stylesheets/pages/subscription.scss
T
Antoine Clausse 1daed4efe4 [web] Add cancel-loss-messaging split test (#36363)
* [web] Add cancel-loss-messaging split test

Populate the cancel confirmation step with plan-specific losses when the
cancel-loss-messaging split test is enabled: "Are you sure you want to
cancel?" heading, the subscription end date, and a card listing what the
user loses moving to the Free plan (compile timeout, AI, collaborators,
history), with the lost benefit struck through next to its Free-plan
replacement.

Shown only to non-trial Student/Standard/Pro individual subscribers with
no other retention offer (trial extension or downgrade upsell) — those
keep the existing confirmation.

Copy matches the mockups in the ticket. No new analytics events; the
existing subscription-page-cancel-button-click / subscription-cancelled
funnel is used.

Part of #35765

* [web] Consolidate ActiveSubscription stories, add cancel confirmation story

Replace the 13 fixture-per-story exports with a single Dashboard story
driven by controls (subscription fixture select, flexible licensing,
cancel-loss-messaging variant), using the CSF factories pattern. Add a
CancelConfirmation story that renders the cancel confirmation step
directly so the cancel-loss-messaging variant can be previewed without
clicking through, with a Professional fixture available to see the Pro
loss rows.

* [web] Use withSplitTests for the ActiveSubscription stories

Register cancel-loss-messaging in defaultSplitTestsArgTypes and drive the
stories' variant from the shared withSplitTests decorator (radio control,
SplitTestContext provided from args) instead of hand-rolled meta tags and
SplitTestProvider. Generalize withSplitTests' typing so it composes with
CSF factories (preview.meta) as well as legacy meta objects.

* [web] Fix Storybook indexing of the ActiveSubscription stories

Storybook's static CSF indexer requires preview.meta() to receive an
object literal, so spread withSplitTests' argTypes/decorators into the
meta literal instead of wrapping the whole config in the helper call.

* [web] Compose ActiveSubscription stories from orthogonal controls

- Wrap the stories in #subscription-dashboard-root so the dashboard's
  scoped styles (e.g. the cancel-loss strikethrough) apply in Storybook
- Replace the single fixture dropdown with independent controls (group,
  plan, period, currency, trial, status, pending plan change, add-ons,
  coupons, pending license change), building the subscription from the
  fixtures' base + patches
- Guard impossible mixes with argTypes `if` conditions: individual-only
  controls hide when group is on, and pendingLicenseChange only shows
  for groups

* [web] Add student plan to the ActiveSubscription story controls

The cancel-loss-messaging variant has student-specific rows (AI
Assistant / 10 collaborators), so the plan control needs a student
option. Groups have no student plans, so for group subscriptions the
student choice maps to the Standard group fixture.

* [web] Provide ol-plans in the ActiveSubscription stories

The cancel confirmation's downgrade branch (monthly Standard is
isEligibleForDowngradeUpsell) looks up paid-personal in ol-plans and
spins forever when the meta tag is missing. Provide ol-plans and
ol-groupPlans from the fixtures, matching the test render helper.

GitOrigin-RevId: e8491c8d39ed059e42ea3442e8d70c3ef304b009
2026-09-17 11:52:02 +00:00

617 lines
11 KiB
SCSS

/**
* MAIN CONTENT
*/
#subscription-dashboard-root {
.hover-highlight {
&:hover,
&:focus {
background-color: var(--neutral-10);
}
}
li.row-link {
display: flex;
border: 0;
padding: 0;
.row-link-inner {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-02) 0;
text-decoration: none;
color: var(--neutral-90);
width: 100%;
}
a.row-link-inner {
&:hover {
background-color: var(--neutral-10);
}
}
}
.cancel-loss-card {
background-color: var(--bg-light-secondary);
border-radius: var(--border-radius-base);
padding: var(--spacing-06);
margin-bottom: var(--spacing-06);
.cancel-loss-card-heading {
@include body-base;
font-weight: 600;
margin-bottom: var(--spacing-04);
}
}
.cancel-loss-list {
list-style: none;
margin: 0;
padding: 0;
.cancel-loss-list-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-04);
& + .cancel-loss-list-item {
margin-top: var(--spacing-04);
}
}
.cancel-loss-icon {
color: var(--content-danger);
}
.cancel-loss-lost {
color: var(--content-danger);
text-decoration: line-through;
}
}
.cancel-loss-actions {
display: flex;
gap: var(--spacing-04);
}
}
/**
* MODALS
*/
.group-subscription-modal-title-discount {
@include body-base;
margin-top: var(--spacing-02);
margin-bottom: unset;
}
.group-subscription-modal {
.circle {
font-size: var(--font-size-06);
border-radius: 50%;
background-color: var(--green-70);
color: white;
white-space: nowrap;
height: 180px;
width: 180px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: var(--spacing-02);
font-weight: bold;
.small {
opacity: 0.85;
}
.circle-subtext {
font-size: var(--font-size-03);
}
}
.legend-as-label {
font-size: var(--font-size-02);
font-weight: 600;
color: var(--content-secondary);
margin-bottom: 0;
}
.educational-discount-badge {
height: 50px;
p {
display: inline-block;
font-weight: bold;
padding-left: var(--spacing-02);
padding-right: var(--spacing-02);
}
.applied {
background-color: rgba($green-70, 0.1);
color: var(--green-70);
}
.ineligible {
background-color: var(--neutral-10);
}
}
}
#change-plan {
table {
@include media-breakpoint-up(lg) {
th:last-child,
td:last-child {
width: 1%; // will expand to fit the content
}
}
@include media-breakpoint-down(lg) {
display: block;
thead {
display: none;
}
tbody,
td,
tr {
display: inline-block;
padding-top: 0;
padding-bottom: 0;
text-align: center;
width: 100%;
}
td:first-child {
padding-top: var(--spacing-07);
}
td:last-child {
padding-top: var(--spacing-03);
padding-bottom: var(--spacing-07);
}
tr {
border-bottom: 1px solid var(--bs-border-color);
td,
th {
border: 0 !important;
}
}
tr:last-child {
border-bottom: 0;
}
}
}
}
.payment-container {
padding-top: var(--spacing-08);
display: flex;
gap: var(--spacing-10);
justify-content: center;
@include media-breakpoint-down(lg) {
flex-direction: column;
align-items: center;
}
.notification {
align-self: flex-start;
}
.payment-review-panel {
width: 400px;
height: 100%;
@include media-breakpoint-down(lg) {
width: 100%;
}
h4 {
color: var(--neutral-70);
font-weight: 600;
@include heading-sm;
}
.price-feature-description {
color: var(--neutral-70);
font-weight: 400;
@include body-base;
h4 {
margin-bottom: var(--spacing-06);
}
.features-list {
margin-top: var(--spacing-08);
}
.price-summary {
.price-summary-line {
display: flex;
justify-content: space-between;
.price-summary-line-width {
width: 70%;
}
}
p {
margin: unset;
}
.price-nowrap {
white-space: nowrap;
}
.price-summary-total-line {
margin-top: var(--spacing-06);
.price-summary-due {
color: var(--green-50);
}
}
}
.price-cancel-anytime {
@include body-xs;
}
.trial-coupon-summary {
padding-top: var(--spacing-08);
.trial-coupon-summary-list-container {
padding-left: 0;
li {
display: flex;
color: var(--neutral-90);
align-items: center;
justify-content: flex-start;
margin-bottom: var(--spacing-04);
gap: var(--spacing-05);
// Translations longer than the panel wrap onto two even lines
// rather than leaving a single word on its own
text-wrap: balance;
@include body-sm;
}
}
}
}
}
.payment-checkout {
padding: var(--spacing-08);
width: 408px;
@include media-breakpoint-down(lg) {
width: 100%;
}
@include media-breakpoint-down(xs) {
padding-left: 0;
padding-right: 0;
}
.address-line-1-tooltip {
font-size: var(--font-size-03);
}
h1 {
@include heading-md;
margin-bottom: var(--spacing-08);
}
.student-disclaimer {
@include body-sm;
}
.paypal-disclaimer {
@include body-sm;
margin: var(--spacing-05) 0 0 0;
}
.form-group {
&:not(.payment-method-toggle) {
margin-bottom: var(--spacing-05);
}
&.address-1 {
margin-bottom: var(--spacing-04);
}
}
.form-check {
margin-bottom: 0; // override bootstrap5 default margin-bottom of 0.125rem
}
.checkout-form-submit {
margin-top: var(--spacing-08);
}
}
&.stripe-payment-container {
.payment-checkout {
.checkout-form-inputs {
display: flex;
flex-direction: column;
gap: 1rem;
}
// Everything under .form-group is to make the form better match
// Stripe's payment inputs
.form-group {
// we can't use "Noto Sans" as the font within PaymentElement,
// so we're using sans-serif for the text elsewhere in the page
font-family: sans-serif;
.form-control {
padding: 10px 12px;
font-size: 16px;
line-height: 20px;
border: 1px solid #e6e6e6;
transition:
box-shadow 0.15s ease,
border-color 0.15s ease;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 2%);
&:focus {
outline: none;
border-color: #046530; // brand primary color
box-shadow:
0 1px 1px rgb(0 0 0 / 3%),
0 3px 6px rgb(0 0 0 / 2%),
0 0 0 3px rgb(11 107 39 / 25%),
0 1px 1px 0 rgb(0 0 0 / 8%);
}
&.is-invalid {
border-color: var(--bs-form-invalid-border-color);
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 10%),
0 0 0 1px var(--bs-form-invalid-border-color);
}
}
.invalid-feedback {
.material-symbols {
display: none;
}
}
}
}
}
}
.group-heading {
display: flex;
align-items: center;
gap: var(--spacing-04);
margin-bottom: var(--spacing-06);
h2,
.heading {
@include heading-lg;
margin: 0;
word-break: break-all;
}
.btn-ghost {
--bs-btn-bg: transparent;
}
}
.change-currency {
margin-top: var(--spacing-02);
.dropdown-item:not(:first-child) {
margin-top: var(--spacing-02);
}
}
.change-currency-toggle {
padding-left: 0;
padding-bottom: 0;
text-decoration: underline;
color: var(--content-secondary);
&:hover,
&:focus,
&:active {
// !important is unfortunately necessary to override btn:first-child:active
color: var(--content-secondary) !important;
}
&:not(:hover) {
text-decoration: underline;
}
// Hide the dropdown caret, BS5 react-bootstrap does not have a prop for this
&::after {
display: none;
}
@include body-xs;
}
.change-currency-dropdown-selected-icon {
position: absolute;
right: 10px;
top: var(--bs-dropdown-item-padding-y);
}
.payment-method-toggle {
.form-check {
&:first-of-type {
margin-bottom: var(--spacing-05);
}
label {
margin-bottom: 0; // override .website-redesign default
}
}
.payment-method-logos {
display: inline-block;
margin-left: var(--spacing-05);
img:not(:last-child) {
margin-right: var(--spacing-04);
}
}
}
.address2-button {
text-decoration: none;
margin-bottom: var(--spacing-05);
padding: 0;
:hover {
text-decoration: underline;
}
.button-content {
justify-content: flex-start;
color: var(--link-web);
}
}
.three-d-secure-container {
> .three-d-secure-recurly-container {
height: 400px;
}
}
.group-subscription-upgrade-features {
.material-symbols {
@include body-sm;
vertical-align: top;
}
.per-user-price {
@include heading-lg;
color: var(--content-primary);
}
.per-user-price-text {
@include body-xs;
}
.feature-list-item {
@include body-sm;
}
}
.group-subscription-upgrade-card {
.title {
@include body-lg;
}
}
.pricing-warning-heading {
@include body-base;
font-weight: 600;
}
.not-student-switch {
font-size: var(--font-size-02);
margin-left: var(--spacing-08);
label {
font-weight: normal;
}
button {
margin: 0 0 0 var(--spacing-03);
padding: 0;
color: var(--link-web);
font-size: var(--font-size-02);
vertical-align: inherit;
.button-content {
color: var(--link-web);
}
}
}
.add-on-card {
display: flex;
align-items: center;
padding: var(--spacing-05);
gap: var(--spacing-05);
border: 1px solid var(--border-divider);
border-radius: var(--border-radius-base);
.add-on-card-icon {
width: 40px;
height: 40px;
}
.add-on-card-content {
display: flex;
flex-direction: column;
}
.heading {
@include body-base;
font-weight: 600;
}
.description {
color: var(--content-secondary);
}
.highlight {
color: var(--content-primary);
}
.add-on-options-toggle {
padding: var(--spacing-04);
font-size: 0;
line-height: 1;
border: none;
border-radius: 50%;
&::after {
content: none;
}
}
}
.payment-summary-card {
@extend .card-gray;
border-radius: var(--border-radius-large);
h3 {
margin-top: 0;
margin-bottom: var(--spacing-08);
font-weight: 600;
}
}
.plan-terms {
@include body-xs;
margin-top: var(--spacing-04);
}