Przeglądaj źródła

[Group Pricing] Add price change warning to cancellation flow (#22640)

* Add price change warning to cancellation flow
* Fixes lint error from translated string
* update pricing effective date

---------

Co-authored-by: Kristina Hjertberg <kristina.hjertberg@overleaf.com>
GitOrigin-RevId: d1b6982ff437d931144feffddcbddd96f5a3405b
MoxAmber 1 rok temu
rodzic
commit
18aa1b4e3b

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -183,6 +183,8 @@
   "cancel": "",
   "cancel_add_on": "",
   "cancel_anytime": "",
+  "cancel_group_price_warning": "",
+  "cancel_group_price_warning_heading": "",
   "cancel_my_account": "",
   "cancel_my_subscription": "",
   "cancel_personal_subscription_first": "",

+ 51 - 24
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx

@@ -15,6 +15,9 @@ import ExtendTrialButton from './extend-trial-button'
 import { useLocation } from '../../../../../../../shared/hooks/use-location'
 import { debugConsole } from '@/utils/debugging'
 import OLButton from '@/features/ui/components/ol/ol-button'
+import moment from 'moment'
+import { getSplitTestVariant } from '@/utils/splitTestUtils'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
 
 const planCodeToDowngradeTo = 'paid-personal'
 
@@ -157,6 +160,8 @@ export function CancelSubscription() {
     isSuccessSecondaryAction ||
     isSuccessCancel
 
+  const groupPricingVariant = getSplitTestVariant('group-pricing-2025')
+
   if (!personalSubscription || !('recurly' in personalSubscription)) return null
 
   const showDowngrade = showDowngradeOption(
@@ -171,6 +176,13 @@ export function CancelSubscription() {
     return <LoadingSpinner />
   }
 
+  const startDate = moment.utc(personalSubscription.recurly.account.created_at)
+  const pricingChangeEffectiveDate = moment.utc('2025-01-08T12:00:00Z')
+  const displayPricingWarning =
+    groupPricingVariant === 'enabled' &&
+    personalSubscription.plan.groupPlan &&
+    startDate.isBefore(pricingChangeEffectiveDate)
+
   async function handleCancelSubscription() {
     try {
       await runAsyncCancel(postJSON(cancelSubscriptionUrl))
@@ -183,29 +195,44 @@ export function CancelSubscription() {
   const showExtendFreeTrial = userCanExtendTrial
 
   return (
-    <div className="text-center">
-      <p>
-        <strong>{t('wed_love_you_to_stay')}</strong>
-      </p>
-
-      {(isErrorCancel || isErrorSecondaryAction) && <GenericErrorAlert />}
-
-      <NotCancelOption
-        showExtendFreeTrial={showExtendFreeTrial}
-        showDowngrade={showDowngrade}
-        isButtonDisabled={isButtonDisabled}
-        isLoadingSecondaryAction={isLoadingSecondaryAction}
-        isSuccessSecondaryAction={isSuccessSecondaryAction}
-        planToDowngradeTo={planToDowngradeTo}
-        runAsyncSecondaryAction={runAsyncSecondaryAction}
-      />
-
-      <ConfirmCancelSubscriptionButton
-        showNoThanks={showExtendFreeTrial || showDowngrade}
-        onClick={handleCancelSubscription}
-        disabled={isButtonDisabled}
-        isLoading={isSuccessCancel || isLoadingCancel}
-      />
-    </div>
+    <>
+      {displayPricingWarning && (
+        <OLNotification
+          type="warning"
+          content={
+            <>
+              <h2 className="pricing-warning-heading">
+                {t('cancel_group_price_warning_heading')}
+              </h2>
+              <p>{t('cancel_group_price_warning')}</p>
+            </>
+          }
+        />
+      )}
+      <div className="text-center">
+        <p>
+          <strong>{t('wed_love_you_to_stay')}</strong>
+        </p>
+
+        {(isErrorCancel || isErrorSecondaryAction) && <GenericErrorAlert />}
+
+        <NotCancelOption
+          showExtendFreeTrial={showExtendFreeTrial}
+          showDowngrade={showDowngrade}
+          isButtonDisabled={isButtonDisabled}
+          isLoadingSecondaryAction={isLoadingSecondaryAction}
+          isSuccessSecondaryAction={isSuccessSecondaryAction}
+          planToDowngradeTo={planToDowngradeTo}
+          runAsyncSecondaryAction={runAsyncSecondaryAction}
+        />
+
+        <ConfirmCancelSubscriptionButton
+          showNoThanks={showExtendFreeTrial || showDowngrade}
+          onClick={handleCancelSubscription}
+          disabled={isButtonDisabled}
+          isLoading={isSuccessCancel || isLoadingCancel}
+        />
+      </div>
+    </>
   )
 }

+ 8 - 0
services/web/frontend/stylesheets/app/subscription.less

@@ -298,3 +298,11 @@ a.row-link {
     color: @content-secondary;
   }
 }
+
+.pricing-warning-heading {
+  font-size: @font-size-03;
+  font-weight: 600;
+  font-family: @font-family-sans-serif;
+  margin-top: 0;
+  color: @content-primary-on-dark-bg;
+}

+ 6 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss

@@ -410,3 +410,9 @@
     @include body-lg;
   }
 }
+
+.pricing-warning-heading {
+  @include body-base;
+
+  font-weight: 600;
+}

+ 2 - 0
services/web/locales/en.json

@@ -248,6 +248,8 @@
   "cancel": "Cancel",
   "cancel_add_on": "Cancel add-on",
   "cancel_anytime": "We’re confident that you’ll love __appName__, but if not you can cancel anytime. We’ll give you your money back, no questions asked, if you let us know within 30 days.",
+  "cancel_group_price_warning": "Our per-user prices are increasing in 2025. Keep your current rate for existing and new users by staying with us.",
+  "cancel_group_price_warning_heading": "Don’t lose your current pricing",
   "cancel_my_account": "Cancel my subscription",
   "cancel_my_subscription": "Cancel my subscription",
   "cancel_personal_subscription_first": "You already have an individual subscription, would you like us to cancel this first before joining the group licence?",

+ 11 - 0
services/web/test/frontend/features/subscription/fixtures/subscriptions.ts

@@ -49,6 +49,7 @@ export const annualActiveSubscription: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -90,6 +91,7 @@ export const annualActiveSubscriptionEuro: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -130,6 +132,7 @@ export const annualActiveSubscriptionPro: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -171,6 +174,7 @@ export const pastDueExpiredSubscription: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'true', $: { type: 'boolean' } },
     },
@@ -212,6 +216,7 @@ export const canceledSubscription: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'true', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -253,6 +258,7 @@ export const pendingSubscriptionChange: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -305,6 +311,7 @@ export const groupActiveSubscription: GroupSubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },
@@ -351,6 +358,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
       activeCoupons: [],
       account: {
         email: 'fake@example.com',
+        created_at: '2024-12-31T09:40:27.000Z',
         has_canceled_subscription: {
           _: 'false',
           $: {
@@ -415,6 +423,7 @@ export const trialSubscription: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'fake@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: {
         _: 'false',
         $: {
@@ -487,6 +496,7 @@ export const trialCollaboratorSubscription: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'foo@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: {
         _: 'false',
         $: {
@@ -537,6 +547,7 @@ export const monthlyActiveCollaborator: RecurlySubscription = {
     activeCoupons: [],
     account: {
       email: 'foo@example.com',
+      created_at: '2024-12-31T09:40:27.000Z',
       has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
       has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
     },

+ 1 - 0
services/web/types/subscription/dashboard/subscription.ts

@@ -26,6 +26,7 @@ type Recurly = {
   activeCoupons: any[] // TODO: confirm type in array
   account: {
     email: string
+    created_at: string
     // data via Recurly API
     has_canceled_subscription: {
       _: 'false' | 'true'