Ver Fonte

Merge pull request #23503 from overleaf/as-compile-timeout-paywall-primary

[web] Create UpgradePrompt paywall component

GitOrigin-RevId: 591eaae0c179c7419f8ec93284cebb3ecfc52154
MoxAmber há 1 ano atrás
pai
commit
420c3881c2

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

@@ -1,4 +1,6 @@
 {
+  "10_editors_per_project": "",
+  "12x_more_compile_time": "",
   "1_2_width": "",
   "1_4_width": "",
   "3_4_width": "",
@@ -162,6 +164,7 @@
   "back_to_editor": "",
   "back_to_subscription": "",
   "back_to_your_projects": "",
+  "basic_compile_time": "",
   "become_an_advisor": "",
   "before_you_use_error_assistant": "",
   "beta_program_already_participating": "",
@@ -271,6 +274,7 @@
   "compact": "",
   "company_name": "",
   "compare": "",
+  "compare_all_plans": "",
   "comparing_from_x_to_y": "",
   "compile_error_entry_description": "",
   "compile_error_handling": "",
@@ -308,6 +312,7 @@
   "continue_github_merge": "",
   "continue_to": "",
   "continue_using_free_features": "",
+  "continue_with_free_plan": "",
   "copied": "",
   "copy": "",
   "copy_code": "",
@@ -558,6 +563,7 @@
   "for_universities": "",
   "format": "",
   "found_matching_deleted_users": "",
+  "free": "",
   "free_7_day_trial_billed_annually": "",
   "free_7_day_trial_billed_monthly": "",
   "free_plan_label": "",
@@ -865,6 +871,7 @@
   "lets_get_those_premium_features": "",
   "library": "",
   "license_for_educational_purposes_confirmation": "",
+  "limited_document_history": "",
   "limited_offer": "",
   "limited_to_n_editors": "",
   "limited_to_n_editors_per_project": "",
@@ -1062,6 +1069,7 @@
   "ok_join_project": "",
   "on": "",
   "on_free_plan_upgrade_to_access_features": "",
+  "one_editor_per_project": "",
   "one_step_away_from_professional_features": "",
   "only_group_admin_or_managers_can_delete_your_account_1": "",
   "only_group_admin_or_managers_can_delete_your_account_2": "",
@@ -1142,6 +1150,7 @@
   "pending_additional_licenses": "",
   "pending_addon_cancellation": "",
   "pending_invite": "",
+  "per_month": "",
   "per_user": "",
   "percent_is_the_percentage_of_the_line_width": "",
   "permanently_disables_the_preview": "",
@@ -1266,6 +1275,7 @@
   "reauthorize_github_account": "",
   "recaptcha_conditions": "",
   "recent_commits_in_github": "",
+  "recommended": "",
   "recompile": "",
   "recompile_from_scratch": "",
   "recompile_pdf": "",
@@ -1555,6 +1565,7 @@
   "sso_user_explanation_enabled_with_group_name": "",
   "sso_user_explanation_ready_with_admin_email": "",
   "sso_user_explanation_ready_with_group_name": "",
+  "standard": "",
   "start_a_free_trial": "",
   "start_by_adding_your_email": "",
   "start_by_fixing_the_first_error_in_your_doc": "",
@@ -1570,6 +1581,7 @@
   "stop_on_validation_error": "",
   "store_your_work": "",
   "stretch_width_to_text": "",
+  "student": "",
   "student_disclaimer": "",
   "subject": "",
   "subject_area": "",
@@ -1813,6 +1825,7 @@
   "unknown_group": "",
   "unlimited_collaborators_per_project": "",
   "unlimited_collabs": "",
+  "unlimited_document_history": "",
   "unlimited_projects": "",
   "unlink": "",
   "unlink_all_users": "",
@@ -2019,6 +2032,7 @@
   "your_affiliation_is_confirmed": "",
   "your_browser_does_not_support_this_feature": "",
   "your_compile_timed_out": "",
+  "your_current_plan": "",
   "your_current_plan_gives_you": "",
   "your_current_plan_supports_up_to_x_users": "",
   "your_current_project_will_revert_to_the_version_from_time": "",

+ 165 - 0
services/web/frontend/js/shared/components/upgrade-prompt.tsx

@@ -0,0 +1,165 @@
+import OLBadge from '@/features/ui/components/ol/ol-badge'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import OLCloseButton from '@/features/ui/components/ol/ol-close-button'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import MaterialIcon from '@/shared/components/material-icon'
+import { PropsWithChildren } from 'react'
+import { Container } from 'react-bootstrap-5'
+import { Trans, useTranslation } from 'react-i18next'
+
+type IconListItemProps = PropsWithChildren<{
+  icon: string
+}>
+
+function IconListItem({ icon, children }: IconListItemProps) {
+  return (
+    <li className="d-flex align-items-center">
+      <div className="flex-shrink-0">
+        <MaterialIcon type={icon} />
+      </div>
+      <div className="flex-grow-1 ms-2">{children}</div>
+    </li>
+  )
+}
+
+type PlansLinkProps = {
+  itmCampaign: string
+}
+function PlansLink({
+  children,
+  itmCampaign,
+}: PropsWithChildren<PlansLinkProps>) {
+  return (
+    <a
+      key="compare_plans_link"
+      href={`/user/subscription/plans&itm-campaign=${itmCampaign}`}
+      target="_blank"
+      rel="noreferrer"
+    >
+      {children}
+      <MaterialIcon type="open_in_new" />
+    </a>
+  )
+}
+
+type UpgradePromptProps = {
+  title: string
+  summary: string
+  onClose: () => void
+  planPricing: { student: string; standard: string }
+  itmCampaign: string
+  isStudent?: boolean
+}
+
+export function UpgradePrompt({
+  title,
+  summary,
+  onClose,
+  planPricing,
+  itmCampaign,
+  isStudent = false,
+}: UpgradePromptProps) {
+  const { t } = useTranslation()
+  const planPrice = isStudent ? planPricing.student : planPricing.standard
+  const planCode = isStudent
+    ? 'student_free_trial_7_days'
+    : 'collaborator_free_trial_7_days'
+
+  return (
+    <Container className="upgrade-prompt">
+      <OLRow className="justify-content-end">
+        <OLCloseButton onClick={() => onClose()} />
+      </OLRow>
+      <OLRow className="text-center">
+        <h2 className="my-0 upgrade-prompt-title">{title}</h2>
+        <p className="upgrade-prompt-summary">{summary}</p>
+      </OLRow>
+      <OLRow className="g-3">
+        <OLCol md={6}>
+          <div className="g-0 upgrade-prompt-card upgrade-prompt-card-premium">
+            <OLRow className="justify-content-between">
+              <OLCol>
+                <h3>{isStudent ? t('student') : t('standard')}</h3>
+              </OLCol>
+              <OLCol xs="auto">
+                <OLBadge className="badge-premium-gradient">
+                  {t('recommended')}
+                </OLBadge>
+              </OLCol>
+            </OLRow>
+            <OLRow>
+              <p className="upgrade-prompt-price">
+                <span className="upgrade-prompt-price-number">{planPrice}</span>{' '}
+                {t('per_month')}
+              </p>
+            </OLRow>
+            <OLRow>
+              <ul className="upgrade-prompt-list">
+                <IconListItem icon="hourglass_top">
+                  {t('12x_more_compile_time')}
+                </IconListItem>
+                <IconListItem icon="group_add">
+                  {t('10_editors_per_project')}
+                </IconListItem>
+                <IconListItem icon="history">
+                  {t('unlimited_document_history')}
+                </IconListItem>
+              </ul>
+            </OLRow>
+            <OLRow className="mt-auto">
+              <a
+                className="btn btn-premium"
+                href={`/user/subscription/new?planCode=${planCode}&itm-campaign=${itmCampaign}`}
+              >
+                {t('try_for_free')}
+              </a>
+            </OLRow>
+          </div>
+        </OLCol>
+        <OLCol md={6}>
+          <div className="g-0 upgrade-prompt-card upgrade-prompt-card-free">
+            <OLRow>
+              <h3>{t('free')}</h3>
+            </OLRow>
+            <OLRow>
+              <p className="upgrade-prompt-price">
+                {/* Invisible span here to hold the correct height to match a card with a price */}
+                <span className="upgrade-prompt-price-number invisible" />
+                {t('your_current_plan')}
+              </p>
+            </OLRow>
+            <OLRow>
+              <ul className="upgrade-prompt-list">
+                <IconListItem icon="hourglass_bottom">
+                  {t('basic_compile_time')}
+                </IconListItem>
+                <IconListItem icon="person">
+                  {t('one_editor_per_project')}
+                </IconListItem>
+                <IconListItem icon="history_off">
+                  {t('limited_document_history')}
+                </IconListItem>
+              </ul>
+            </OLRow>
+            <OLRow className="mt-auto">
+              <OLButton variant="secondary" onClick={() => onClose()}>
+                {t('continue_with_free_plan')}
+              </OLButton>
+            </OLRow>
+          </div>
+        </OLCol>
+      </OLRow>
+      <OLRow className="text-center">
+        <p className="upgrade-prompt-all-plans">
+          {/* eslint-disable react/jsx-key */}
+          <Trans
+            i18nKey="compare_all_plans"
+            components={[<PlansLink itmCampaign={itmCampaign} />]}
+          />
+          {/* eslint-disable react/jsx-key */}
+        </p>
+      </OLRow>
+    </Container>
+  )
+}

+ 24 - 0
services/web/frontend/stories/upgrade-prompt.stories.tsx

@@ -0,0 +1,24 @@
+import { UpgradePrompt } from '@/shared/components/upgrade-prompt'
+import { StoryObj } from '@storybook/react/*'
+import { bsVersionDecorator } from '../../.storybook/utils/with-bootstrap-switcher'
+
+type Story = StoryObj<typeof UpgradePrompt>
+
+export const Generic: Story = {
+  args: {
+    title: 'Unlock more compile time',
+    summary: 'Your project took too long to compile and timed out.',
+    onClose: () => {},
+    planPricing: { student: '$9', standard: '$21' },
+    itmCampaign: 'storybook',
+  },
+}
+
+export default {
+  title: 'Shared / Components / Upgrade Prompt',
+  component: UpgradePrompt,
+  ...bsVersionDecorator,
+  args: {
+    bootstrapVersion: 5,
+  },
+}

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/all.scss

@@ -41,3 +41,4 @@
 @import 'panel-heading';
 @import 'menu-bar';
 @import 'invite';
+@import 'upgrade-prompt';

+ 8 - 0
services/web/frontend/stylesheets/bootstrap-5/components/badge.scss

@@ -108,6 +108,14 @@ $max-width: 160px;
   font-weight: var(--badge-font-weight);
 }
 
+.badge-premium-gradient {
+  background: var(--premium-gradient);
+  color: var(--content-primary-dark);
+  vertical-align: middle;
+  margin-left: 5px;
+  font-weight: var(--badge-font-weight);
+}
+
 .badge-link {
   display: inline-block;
 

+ 77 - 0
services/web/frontend/stylesheets/bootstrap-5/components/upgrade-prompt.scss

@@ -0,0 +1,77 @@
+.upgrade-prompt {
+  border-radius: var(--border-radius-base);
+  box-shadow: 0 4px 6px 0 #1e25301f;
+  padding: var(--spacing-06);
+
+  .row {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  .upgrade-prompt-title {
+    font-weight: 600;
+  }
+
+  .upgrade-prompt-summary {
+    margin-bottom: var(--spacing-10);
+  }
+
+  .upgrade-prompt-card {
+    display: flex;
+    flex-direction: column;
+    border-radius: var(--border-radius-medium);
+    padding: var(--spacing-09);
+    height: 100%;
+    font-size: var(--font-size-02);
+
+    h3 {
+      margin: 0;
+      font-weight: 600;
+    }
+  }
+
+  .upgrade-prompt-card-premium {
+    border: 2px solid transparent;
+    background:
+      linear-gradient(white, white) padding-box,
+      var(--premium-gradient) border-box;
+  }
+
+  .upgrade-prompt-card-free {
+    border: 2px solid var(--border-divider);
+  }
+
+  .upgrade-prompt-price {
+    margin-bottom: var(--spacing-05);
+  }
+
+  .upgrade-prompt-price-number {
+    @include heading-xl;
+
+    font-weight: 600;
+  }
+
+  .upgrade-prompt-list {
+    margin-bottom: var(--spacing-08);
+
+    & .material-symbols {
+      vertical-align: bottom;
+    }
+
+    & li:not(:last-of-type) {
+      margin-bottom: var(--spacing-05);
+    }
+  }
+
+  .upgrade-prompt-all-plans {
+    @include body-xs;
+
+    margin-top: var(--spacing-07);
+    margin-bottom: 0;
+
+    & .material-symbols {
+      vertical-align: bottom;
+      font-size: var(--font-size-02);
+    }
+  }
+}

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

@@ -1,5 +1,7 @@
 {
+  "10_editors_per_project": "10 editors per project",
   "12x_basic": "12x Basic",
+  "12x_more_compile_time": "12x more compile time on our fastest servers",
   "1_2_width": "½ width",
   "1_4_width": "¼ width",
   "3_4_width": "¾ width",
@@ -206,6 +208,7 @@
   "back_to_subscription": "Back to Subscription",
   "back_to_your_projects": "Back to your projects",
   "basic": "Basic",
+  "basic_compile_time": "Basic compile time",
   "basic_compile_timeout_on_fast_servers": "Basic compile timeout on fast servers",
   "become_an_advisor": "Become an __appName__ advisor",
   "before_you_use_error_assistant": "Before you use Error Assist",
@@ -352,6 +355,7 @@
   "compact": "Compact",
   "company_name": "Company Name",
   "compare": "Compare",
+  "compare_all_plans": "Compare all plans on our <0>pricing page</0>",
   "compare_features": "Compare features",
   "comparing_from_x_to_y": "Comparing from <0>__startTime__</0> to <0>__endTime__</0>",
   "compile_error_entry_description": "An error which prevented this project from compiling",
@@ -1149,6 +1153,7 @@
   "library": "Library",
   "license": "License",
   "license_for_educational_purposes_confirmation": "<0>__percent__% educational discount</0><1/>I confirm this subscription is for educational purposes (applies to students or faculty using __appName__ for teaching)",
+  "limited_document_history": "Limited document history",
   "limited_to_n_editors": "Limited to __count__ editor",
   "limited_to_n_editors_per_project": "Limited to __count__ editor per project",
   "limited_to_n_editors_per_project_plural": "Limited to __count__ editors per project",
@@ -1417,6 +1422,7 @@
   "on": "On",
   "on_free_plan_upgrade_to_access_features": "You are on the __appName__ Free plan. Upgrade to access these <0>Premium features</0>",
   "one_collaborator_per_project": "1 collaborator per project",
+  "one_editor_per_project": "1 editor per project",
   "one_free_collab": "One free collaborator",
   "one_per_project": "1 per project",
   "one_step_away_from_professional_features": "You are one step away from accessing <0>Overleaf Professional features</0>!",
@@ -1686,6 +1692,7 @@
   "recaptcha_conditions": "The site is protected by reCAPTCHA and the Google <1>Privacy Policy</1> and <2>Terms of Service</2> apply.",
   "recent": "Recent",
   "recent_commits_in_github": "Recent commits in GitHub",
+  "recommended": "Recommended",
   "recompile": "Recompile",
   "recompile_from_scratch": "Recompile from scratch",
   "recompile_pdf": "Recompile the PDF",
@@ -2346,6 +2353,7 @@
   "unlimited": "Unlimited",
   "unlimited_collaborators_per_project": "Unlimited collaborators per project",
   "unlimited_collabs": "Unlimited collaborators",
+  "unlimited_document_history": "Unlimited document history",
   "unlimited_projects": "Unlimited projects",
   "unlink": "Unlink",
   "unlink_all_users": "Unlink all users",
@@ -2580,6 +2588,7 @@
   "your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
   "your_browser_does_not_support_this_feature": "Sorry, your browser doesn’t support this feature. Please update your browser to its latest version.",
   "your_compile_timed_out": "Your compile timed out",
+  "your_current_plan": "Your current plan",
   "your_current_plan_gives_you": "By pausing your subscription, you’ll be able to access your premium features faster when you need them again.",
   "your_current_plan_supports_up_to_x_users": "Your current plan supports up to __users__ users.",
   "your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__",