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

[web] Create UpgradePrompt paywall component

GitOrigin-RevId: 591eaae0c179c7419f8ec93284cebb3ecfc52154
This commit is contained in:
MoxAmber
2025-02-14 09:04:07 +00:00
committed by Copybot
parent be33d0dd67
commit 420c3881c2
7 changed files with 298 additions and 0 deletions
@@ -1,4 +1,6 @@
{ {
"10_editors_per_project": "",
"12x_more_compile_time": "",
"1_2_width": "", "1_2_width": "",
"1_4_width": "", "1_4_width": "",
"3_4_width": "", "3_4_width": "",
@@ -162,6 +164,7 @@
"back_to_editor": "", "back_to_editor": "",
"back_to_subscription": "", "back_to_subscription": "",
"back_to_your_projects": "", "back_to_your_projects": "",
"basic_compile_time": "",
"become_an_advisor": "", "become_an_advisor": "",
"before_you_use_error_assistant": "", "before_you_use_error_assistant": "",
"beta_program_already_participating": "", "beta_program_already_participating": "",
@@ -271,6 +274,7 @@
"compact": "", "compact": "",
"company_name": "", "company_name": "",
"compare": "", "compare": "",
"compare_all_plans": "",
"comparing_from_x_to_y": "", "comparing_from_x_to_y": "",
"compile_error_entry_description": "", "compile_error_entry_description": "",
"compile_error_handling": "", "compile_error_handling": "",
@@ -308,6 +312,7 @@
"continue_github_merge": "", "continue_github_merge": "",
"continue_to": "", "continue_to": "",
"continue_using_free_features": "", "continue_using_free_features": "",
"continue_with_free_plan": "",
"copied": "", "copied": "",
"copy": "", "copy": "",
"copy_code": "", "copy_code": "",
@@ -558,6 +563,7 @@
"for_universities": "", "for_universities": "",
"format": "", "format": "",
"found_matching_deleted_users": "", "found_matching_deleted_users": "",
"free": "",
"free_7_day_trial_billed_annually": "", "free_7_day_trial_billed_annually": "",
"free_7_day_trial_billed_monthly": "", "free_7_day_trial_billed_monthly": "",
"free_plan_label": "", "free_plan_label": "",
@@ -865,6 +871,7 @@
"lets_get_those_premium_features": "", "lets_get_those_premium_features": "",
"library": "", "library": "",
"license_for_educational_purposes_confirmation": "", "license_for_educational_purposes_confirmation": "",
"limited_document_history": "",
"limited_offer": "", "limited_offer": "",
"limited_to_n_editors": "", "limited_to_n_editors": "",
"limited_to_n_editors_per_project": "", "limited_to_n_editors_per_project": "",
@@ -1062,6 +1069,7 @@
"ok_join_project": "", "ok_join_project": "",
"on": "", "on": "",
"on_free_plan_upgrade_to_access_features": "", "on_free_plan_upgrade_to_access_features": "",
"one_editor_per_project": "",
"one_step_away_from_professional_features": "", "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_1": "",
"only_group_admin_or_managers_can_delete_your_account_2": "", "only_group_admin_or_managers_can_delete_your_account_2": "",
@@ -1142,6 +1150,7 @@
"pending_additional_licenses": "", "pending_additional_licenses": "",
"pending_addon_cancellation": "", "pending_addon_cancellation": "",
"pending_invite": "", "pending_invite": "",
"per_month": "",
"per_user": "", "per_user": "",
"percent_is_the_percentage_of_the_line_width": "", "percent_is_the_percentage_of_the_line_width": "",
"permanently_disables_the_preview": "", "permanently_disables_the_preview": "",
@@ -1266,6 +1275,7 @@
"reauthorize_github_account": "", "reauthorize_github_account": "",
"recaptcha_conditions": "", "recaptcha_conditions": "",
"recent_commits_in_github": "", "recent_commits_in_github": "",
"recommended": "",
"recompile": "", "recompile": "",
"recompile_from_scratch": "", "recompile_from_scratch": "",
"recompile_pdf": "", "recompile_pdf": "",
@@ -1555,6 +1565,7 @@
"sso_user_explanation_enabled_with_group_name": "", "sso_user_explanation_enabled_with_group_name": "",
"sso_user_explanation_ready_with_admin_email": "", "sso_user_explanation_ready_with_admin_email": "",
"sso_user_explanation_ready_with_group_name": "", "sso_user_explanation_ready_with_group_name": "",
"standard": "",
"start_a_free_trial": "", "start_a_free_trial": "",
"start_by_adding_your_email": "", "start_by_adding_your_email": "",
"start_by_fixing_the_first_error_in_your_doc": "", "start_by_fixing_the_first_error_in_your_doc": "",
@@ -1570,6 +1581,7 @@
"stop_on_validation_error": "", "stop_on_validation_error": "",
"store_your_work": "", "store_your_work": "",
"stretch_width_to_text": "", "stretch_width_to_text": "",
"student": "",
"student_disclaimer": "", "student_disclaimer": "",
"subject": "", "subject": "",
"subject_area": "", "subject_area": "",
@@ -1813,6 +1825,7 @@
"unknown_group": "", "unknown_group": "",
"unlimited_collaborators_per_project": "", "unlimited_collaborators_per_project": "",
"unlimited_collabs": "", "unlimited_collabs": "",
"unlimited_document_history": "",
"unlimited_projects": "", "unlimited_projects": "",
"unlink": "", "unlink": "",
"unlink_all_users": "", "unlink_all_users": "",
@@ -2019,6 +2032,7 @@
"your_affiliation_is_confirmed": "", "your_affiliation_is_confirmed": "",
"your_browser_does_not_support_this_feature": "", "your_browser_does_not_support_this_feature": "",
"your_compile_timed_out": "", "your_compile_timed_out": "",
"your_current_plan": "",
"your_current_plan_gives_you": "", "your_current_plan_gives_you": "",
"your_current_plan_supports_up_to_x_users": "", "your_current_plan_supports_up_to_x_users": "",
"your_current_project_will_revert_to_the_version_from_time": "", "your_current_project_will_revert_to_the_version_from_time": "",
@@ -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>
)
}
@@ -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,
},
}
@@ -41,3 +41,4 @@
@import 'panel-heading'; @import 'panel-heading';
@import 'menu-bar'; @import 'menu-bar';
@import 'invite'; @import 'invite';
@import 'upgrade-prompt';
@@ -108,6 +108,14 @@ $max-width: 160px;
font-weight: var(--badge-font-weight); 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 { .badge-link {
display: inline-block; display: inline-block;
@@ -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
View File
@@ -1,5 +1,7 @@
{ {
"10_editors_per_project": "10 editors per project",
"12x_basic": "12x Basic", "12x_basic": "12x Basic",
"12x_more_compile_time": "12x more compile time on our fastest servers",
"1_2_width": "½ width", "1_2_width": "½ width",
"1_4_width": "¼ width", "1_4_width": "¼ width",
"3_4_width": "¾ width", "3_4_width": "¾ width",
@@ -206,6 +208,7 @@
"back_to_subscription": "Back to Subscription", "back_to_subscription": "Back to Subscription",
"back_to_your_projects": "Back to your projects", "back_to_your_projects": "Back to your projects",
"basic": "Basic", "basic": "Basic",
"basic_compile_time": "Basic compile time",
"basic_compile_timeout_on_fast_servers": "Basic compile timeout on fast servers", "basic_compile_timeout_on_fast_servers": "Basic compile timeout on fast servers",
"become_an_advisor": "Become an __appName__ advisor", "become_an_advisor": "Become an __appName__ advisor",
"before_you_use_error_assistant": "Before you use Error Assist", "before_you_use_error_assistant": "Before you use Error Assist",
@@ -352,6 +355,7 @@
"compact": "Compact", "compact": "Compact",
"company_name": "Company Name", "company_name": "Company Name",
"compare": "Compare", "compare": "Compare",
"compare_all_plans": "Compare all plans on our <0>pricing page</0>",
"compare_features": "Compare features", "compare_features": "Compare features",
"comparing_from_x_to_y": "Comparing from <0>__startTime__</0> to <0>__endTime__</0>", "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", "compile_error_entry_description": "An error which prevented this project from compiling",
@@ -1149,6 +1153,7 @@
"library": "Library", "library": "Library",
"license": "License", "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)", "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": "Limited to __count__ editor",
"limited_to_n_editors_per_project": "Limited to __count__ editor per project", "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", "limited_to_n_editors_per_project_plural": "Limited to __count__ editors per project",
@@ -1417,6 +1422,7 @@
"on": "On", "on": "On",
"on_free_plan_upgrade_to_access_features": "You are on the __appName__ Free plan. Upgrade to access these <0>Premium features</0>", "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_collaborator_per_project": "1 collaborator per project",
"one_editor_per_project": "1 editor per project",
"one_free_collab": "One free collaborator", "one_free_collab": "One free collaborator",
"one_per_project": "1 per project", "one_per_project": "1 per project",
"one_step_away_from_professional_features": "You are one step away from accessing <0>Overleaf Professional features</0>!", "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.", "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": "Recent",
"recent_commits_in_github": "Recent commits in GitHub", "recent_commits_in_github": "Recent commits in GitHub",
"recommended": "Recommended",
"recompile": "Recompile", "recompile": "Recompile",
"recompile_from_scratch": "Recompile from scratch", "recompile_from_scratch": "Recompile from scratch",
"recompile_pdf": "Recompile the PDF", "recompile_pdf": "Recompile the PDF",
@@ -2346,6 +2353,7 @@
"unlimited": "Unlimited", "unlimited": "Unlimited",
"unlimited_collaborators_per_project": "Unlimited collaborators per project", "unlimited_collaborators_per_project": "Unlimited collaborators per project",
"unlimited_collabs": "Unlimited collaborators", "unlimited_collabs": "Unlimited collaborators",
"unlimited_document_history": "Unlimited document history",
"unlimited_projects": "Unlimited projects", "unlimited_projects": "Unlimited projects",
"unlink": "Unlink", "unlink": "Unlink",
"unlink_all_users": "Unlink all users", "unlink_all_users": "Unlink all users",
@@ -2580,6 +2588,7 @@
"your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.", "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_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_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_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_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__", "your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__",