From 70bae34bd84fab604855f131b773100cdd935502 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Tue, 2 May 2023 15:00:45 +0100 Subject: [PATCH] Add paywall to React history view (#12849) * Implement history view paywall * Add tests and some CSS fallbacks * Make additional faded version above paywall non-clickable * Change isFaded to faded for consistency * Remove unused import * Add missing attribute * SHow all labels in free tier * Address review comments * Change Boolean conversion Co-authored-by: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> * Make adding or deleting a label show up in version list again * Refactor to use visibleUpdateCount rather than maintaining two separate update arrays * Removed unused import * Use data-testid instead on class * Round gradient values * Correct test selector --------- Co-authored-by: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> GitOrigin-RevId: a2b021f3f4d3b9eb1358edb2ee4aa7db1bc7240e --- .../web/frontend/extracted-translations.json | 2 + .../change-list/all-history-list.tsx | 30 ++- .../change-list/history-version-details.tsx | 5 +- .../change-list/history-version.tsx | 32 ++- .../components/change-list/labels-list.tsx | 1 + .../change-list/non-owner-paywall-prompt.tsx | 15 ++ .../change-list/owner-paywall-prompt.tsx | 60 +++++ .../history/context/history-context.tsx | 61 +++-- .../context/types/history-context-value.ts | 4 +- .../js/shared/context/project-context.js | 1 + .../stylesheets/app/editor/history-react.less | 35 ++- .../stylesheets/components/buttons.less | 6 + .../web/frontend/stylesheets/core/mixins.less | 19 ++ .../frontend/stylesheets/core/variables.less | 10 + .../frontend/stylesheets/variables/all.less | 8 + .../history/components/change-list.spec.tsx | 214 +++++++++++++++++- .../test/frontend/helpers/editor-providers.js | 9 +- 17 files changed, 448 insertions(+), 64 deletions(-) create mode 100644 services/web/frontend/js/features/history/components/change-list/non-owner-paywall-prompt.tsx create mode 100644 services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index db0c9c69ab..9b2badac01 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -50,6 +50,7 @@ "are_you_affiliated_with_an_institution": "", "are_you_still_at": "", "ascending": "", + "ask_proj_owner_to_upgrade_for_full_history": "", "ask_proj_owner_to_upgrade_for_git_bridge": "", "ask_proj_owner_to_upgrade_for_longer_compiles": "", "ask_proj_owner_to_upgrade_for_references_search": "", @@ -161,6 +162,7 @@ "created_at": "", "creating": "", "current_password": "", + "currently_seeing_only_24_hrs_history": "", "currently_subscribed_to_plan": "", "customize_your_group_subscription": "", "date_and_owner": "", diff --git a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx index 3b08bff9a6..48e59ef7d9 100644 --- a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx @@ -2,15 +2,27 @@ import { Fragment, useEffect, useRef, useState } from 'react' import { useHistoryContext } from '../../context/history-context' import HistoryVersion from './history-version' import LoadingSpinner from '../../../../shared/components/loading-spinner' +import { OwnerPaywallPrompt } from './owner-paywall-prompt' +import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt' function AllHistoryList() { - const { updatesInfo, loadingState, fetchNextBatchOfUpdates } = - useHistoryContext() - const { updates, atEnd } = updatesInfo + const { + updatesInfo, + loadingState, + fetchNextBatchOfUpdates, + currentUserIsOwner, + } = useHistoryContext() + const { visibleUpdateCount, updates, atEnd } = updatesInfo const scrollerRef = useRef(null) const bottomRef = useRef(null) const intersectionObserverRef = useRef(null) const [bottomVisible, setBottomVisible] = useState(false) + const showPaywall = + loadingState === 'ready' && updatesInfo.freeHistoryLimitHit + const showOwnerPaywall = showPaywall && currentUserIsOwner + const showNonOwnerPaywall = showPaywall && !currentUserIsOwner + const visibleUpdates = + visibleUpdateCount === null ? updates : updates.slice(0, visibleUpdateCount) // Create an intersection observer that watches for any part of an element // positioned at the bottom of the list to be visible @@ -63,15 +75,23 @@ function AllHistoryList() {
- {updates.map((update, index) => ( + {visibleUpdates.map((update, index) => ( {update.meta.first_in_day && index > 0 && (
)} - +
))}
+ {showOwnerPaywall ? : null} + {showNonOwnerPaywall ? : null} {loadingState === 'ready' ? null : }
) diff --git a/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx b/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx index d73276f296..ab3dc6b1c3 100644 --- a/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx +++ b/services/web/frontend/js/features/history/components/change-list/history-version-details.tsx @@ -5,11 +5,13 @@ import { UpdateRange } from '../../services/types/update' type HistoryVersionDetailsProps = { children: React.ReactNode selected: boolean + selectable: boolean } & UpdateRange function HistoryVersionDetails({ children, selected, + selectable, fromV, toV, fromVTimestamp, @@ -34,9 +36,10 @@ function HistoryVersionDetails({
{children}
diff --git a/services/web/frontend/js/features/history/components/change-list/history-version.tsx b/services/web/frontend/js/features/history/components/change-list/history-version.tsx index e11dcf5d33..63c4c0221a 100644 --- a/services/web/frontend/js/features/history/components/change-list/history-version.tsx +++ b/services/web/frontend/js/features/history/components/change-list/history-version.tsx @@ -10,12 +10,14 @@ import { isVersionSelected } from '../../utils/history-details' import { relativeDate, formatTime } from '../../../utils/format-date' import { orderBy } from 'lodash' import { LoadedUpdate } from '../../services/types/update' +import classNames from 'classnames' type HistoryEntryProps = { update: LoadedUpdate + faded: boolean } -function HistoryVersion({ update }: HistoryEntryProps) { +function HistoryVersion({ update, faded }: HistoryEntryProps) { const { id: currentUserId } = useUserContext() const { projectId, selection } = useHistoryContext() @@ -23,7 +25,12 @@ function HistoryVersion({ update }: HistoryEntryProps) { const selected = isVersionSelected(selection, update.fromV, update.toV) return ( -
+
{update.meta.first_in_day && (
) diff --git a/services/web/frontend/js/features/history/components/change-list/labels-list.tsx b/services/web/frontend/js/features/history/components/change-list/labels-list.tsx index f2d4f5ae74..b5376cb25a 100644 --- a/services/web/frontend/js/features/history/components/change-list/labels-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/labels-list.tsx @@ -44,6 +44,7 @@ function LabelsList() { fromVTimestamp={fromVTimestamp} toVTimestamp={toVTimestamp} selected={selected} + selectable >
{labels.map(label => ( diff --git a/services/web/frontend/js/features/history/components/change-list/non-owner-paywall-prompt.tsx b/services/web/frontend/js/features/history/components/change-list/non-owner-paywall-prompt.tsx new file mode 100644 index 0000000000..583fed2cac --- /dev/null +++ b/services/web/frontend/js/features/history/components/change-list/non-owner-paywall-prompt.tsx @@ -0,0 +1,15 @@ +import { useTranslation } from 'react-i18next' + +export function NonOwnerPaywallPrompt() { + const { t } = useTranslation() + + return ( +
+

{t('premium_feature')}

+

{t('currently_seeing_only_24_hrs_history')}

+

+ {t('ask_proj_owner_to_upgrade_for_full_history')} +

+
+ ) +} diff --git a/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx new file mode 100644 index 0000000000..c057cea145 --- /dev/null +++ b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from 'react-i18next' +import Icon from '../../../../shared/components/icon' +import { useCallback, useEffect, useState } from 'react' +import * as eventTracking from '../../../../infrastructure/event-tracking' +import StartFreeTrialButton from '../../../../shared/components/start-free-trial-button' +import { paywallPrompt } from '../../../../main/account-upgrade' + +function FeatureItem({ text }: { text: string }) { + return ( +
  • + {text} +
  • + ) +} + +export function OwnerPaywallPrompt() { + const { t } = useTranslation() + const [clickedFreeTrialButton, setClickedFreeTrialButton] = useState(false) + + useEffect(() => { + eventTracking.send('subscription-funnel', 'editor-click-feature', 'history') + paywallPrompt('history') + }, []) + + const handleFreeTrialClick = useCallback(() => { + setClickedFreeTrialButton(true) + }, []) + + return ( +
    +

    {t('premium_feature')}

    +

    {t('currently_seeing_only_24_hrs_history')}

    +

    + + {t('upgrade_to_get_feature', { feature: 'full Project History' })} + +

    +
      + + + + + + +
    +

    + +

    + {clickedFreeTrialButton ? ( +

    {t('refresh_page_after_starting_free_trial')}

    + ) : null} +
    + ) +} diff --git a/services/web/frontend/js/features/history/context/history-context.tsx b/services/web/frontend/js/features/history/context/history-context.tsx index 7e440d96ec..4340d4192e 100644 --- a/services/web/frontend/js/features/history/context/history-context.tsx +++ b/services/web/frontend/js/features/history/context/history-context.tsx @@ -18,14 +18,12 @@ import { loadLabels } from '../utils/label' import { autoSelectFile } from '../utils/auto-select-file' import ColorManager from '../../../ide/colors/ColorManager' import moment from 'moment' -import * as eventTracking from '../../../infrastructure/event-tracking' import { cloneDeep } from 'lodash' import { FetchUpdatesResponse, LoadedUpdate, Update, } from '../services/types/update' -import { Nullable } from '../../../../../types/utils' import { Selection } from '../services/types/selection' // Allow testing of infinite scrolling by providing query string parameters to @@ -71,6 +69,10 @@ function useHistory() { const userId = user.id const projectId = project._id const projectOwnerId = project.owner?._id + const userHasFullFeature = Boolean( + project.features?.versioning || user.isAdmin + ) + const currentUserIsOwner = projectOwnerId === userId const [selection, setSelection] = useState(selectionInitialState) @@ -78,6 +80,7 @@ function useHistory() { HistoryContextValue['updatesInfo'] >({ updates: [], + visibleUpdateCount: null, atEnd: false, freeHistoryLimitHit: false, nextBeforeTimestamp: undefined, @@ -86,19 +89,15 @@ function useHistory() { const [loadingState, setLoadingState] = useState('loadingInitial') const [error, setError] = useState(null) - // eslint-disable-next-line no-unused-vars - const [userHasFullFeature, setUserHasFullFeature] = - useState(undefined) const fetchNextBatchOfUpdates = useCallback(() => { const loadUpdates = (updatesData: Update[]) => { const dateTimeNow = new Date() const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1) - let { updates, freeHistoryLimitHit } = updatesInfo + let { updates, freeHistoryLimitHit, visibleUpdateCount } = updatesInfo let previousUpdate = updates[updates.length - 1] - let cutOffIndex: Nullable = null - let loadedUpdates: LoadedUpdate[] = cloneDeep(updatesData) + const loadedUpdates: LoadedUpdate[] = cloneDeep(updatesData) for (const [index, update] of loadedUpdates.entries()) { for (const user of update.meta.users) { if (user) { @@ -114,28 +113,24 @@ function useHistory() { previousUpdate = update - if (userHasFullFeature && update.meta.end_ts < timestamp24hoursAgo) { - cutOffIndex = index || 1 // Make sure that we show at least one entry (to allow labelling). + if ( + !userHasFullFeature && + visibleUpdateCount === null && + update.meta.end_ts < timestamp24hoursAgo + ) { + // Make sure that we show at least one entry (to allow labelling), and + // load one extra update that is displayed but faded out above the + // paywall + visibleUpdateCount = index + 1 freeHistoryLimitHit = true - if (projectOwnerId === userId) { - eventTracking.send( - 'subscription-funnel', - 'editor-click-feature', - 'history' - ) - eventTracking.sendMB('paywall-prompt', { - 'paywall-type': 'history', - }) - } - break } } - if (!userHasFullFeature && cutOffIndex != null) { - loadedUpdates = loadedUpdates.slice(0, cutOffIndex) + return { + updates: updates.concat(loadedUpdates), + visibleUpdateCount, + freeHistoryLimitHit, } - - return { updates: updates.concat(loadedUpdates), freeHistoryLimitHit } } if (updatesInfo.atEnd || loadingState === 'loadingUpdates') return @@ -156,13 +151,15 @@ function useHistory() { setLabels(loadLabels(labels, lastUpdateToV)) } - const { updates, freeHistoryLimitHit } = loadUpdates(updatesData) + const { updates, visibleUpdateCount, freeHistoryLimitHit } = + loadUpdates(updatesData) const atEnd = nextBeforeTimestamp == null || freeHistoryLimitHit || !updates.length setUpdatesInfo({ updates, + visibleUpdateCount, freeHistoryLimitHit, atEnd, nextBeforeTimestamp, @@ -175,15 +172,7 @@ function useHistory() { .finally(() => { setLoadingState('ready') }) - }, [ - loadingState, - labels, - projectId, - projectOwnerId, - userId, - userHasFullFeature, - updatesInfo, - ]) + }, [loadingState, labels, projectId, userHasFullFeature, updatesInfo]) const resetSelection = useCallback(() => { setSelection(selectionInitialState) @@ -257,6 +246,7 @@ function useHistory() { labels, setLabels, userHasFullFeature, + currentUserIsOwner, projectId, selection, setSelection, @@ -272,6 +262,7 @@ function useHistory() { labels, setLabels, userHasFullFeature, + currentUserIsOwner, projectId, selection, setSelection, diff --git a/services/web/frontend/js/features/history/context/types/history-context-value.ts b/services/web/frontend/js/features/history/context/types/history-context-value.ts index 64098de522..71fe67e9cb 100644 --- a/services/web/frontend/js/features/history/context/types/history-context-value.ts +++ b/services/web/frontend/js/features/history/context/types/history-context-value.ts @@ -12,6 +12,7 @@ type LoadingState = export type HistoryContextValue = { updatesInfo: { updates: LoadedUpdate[] + visibleUpdateCount: Nullable atEnd: boolean nextBeforeTimestamp: number | undefined freeHistoryLimitHit: boolean @@ -19,7 +20,8 @@ export type HistoryContextValue = { setUpdatesInfo: React.Dispatch< React.SetStateAction > - userHasFullFeature: boolean | undefined + userHasFullFeature: boolean + currentUserIsOwner: boolean loadingState: LoadingState setLoadingState: React.Dispatch< React.SetStateAction diff --git a/services/web/frontend/js/shared/context/project-context.js b/services/web/frontend/js/shared/context/project-context.js index f41a1bcbc0..91e63a44de 100644 --- a/services/web/frontend/js/shared/context/project-context.js +++ b/services/web/frontend/js/shared/context/project-context.js @@ -25,6 +25,7 @@ export const projectShape = { references: PropTypes.bool, mendeley: PropTypes.bool, zotero: PropTypes.bool, + versioning: PropTypes.bool, }), publicAccessLevel: PropTypes.string, tokens: PropTypes.shape({ diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less index 407f2d0130..9f080f39a3 100644 --- a/services/web/frontend/stylesheets/app/editor/history-react.less +++ b/services/web/frontend/stylesheets/app/editor/history-react.less @@ -1,5 +1,6 @@ @versions-list-width: 320px; @history-toolbar-height: 40px; +@history-change-list-padding: 16px; history-root { height: 100%; @@ -78,7 +79,7 @@ history-root { .history-toggle-switch-container, .history-version-day, .history-version-details { - padding: 0 16px; + padding: 0 @history-change-list-padding; } .history-version-day { @@ -98,7 +99,7 @@ history-root { padding-top: 8px; padding-bottom: 8px; - &:hover { + &.history-version-selectable:hover { cursor: pointer; background-color: @neutral-10; } @@ -267,6 +268,36 @@ history-root { margin-left: 5px; } } + + .history-paywall-prompt { + padding: @history-change-list-padding; + + .history-feature-list { + list-style: none; + padding-left: 8px; + + li { + margin-bottom: 16px; + } + } + + button { + width: 100%; + } + } + + .history-version-faded .history-version-details { + max-height: 6em; + .mask-image(linear-gradient(black 35%, transparent)); + overflow: hidden; + } + + .history-paywall-heading { + .premium-text; + font-family: inherit; + font-size: 20px; + font-weight: 700; + } } .history-version-label-tooltip { diff --git a/services/web/frontend/stylesheets/components/buttons.less b/services/web/frontend/stylesheets/components/buttons.less index 4552708844..782717eedb 100755 --- a/services/web/frontend/stylesheets/components/buttons.less +++ b/services/web/frontend/stylesheets/components/buttons.less @@ -176,6 +176,12 @@ .btn-secondary; } +// Buttons relating to premium features have a special background +.btn-premium { + .premium-background; + color: @white; +} + .reset-btns { // reset all buttons back to their original colors .btn-danger { diff --git a/services/web/frontend/stylesheets/core/mixins.less b/services/web/frontend/stylesheets/core/mixins.less index 6d8beb097d..998d394a0a 100755 --- a/services/web/frontend/stylesheets/core/mixins.less +++ b/services/web/frontend/stylesheets/core/mixins.less @@ -448,6 +448,25 @@ filter: e(%('progid:DXImageTransform.Microsoft.gradient(enabled = false)')); } +.mask-image(@gradient) { + // It has to be this way round, otherwise Less removes the prefixed version, for some reason + mask-image: @gradient; + -webkit-mask-image: @gradient; +} + +.premium-background { + background-image: @premium-gradient; +} + +.premium-text { + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + color: @white; // Fallback + background-color: @blue-70; // Fallback + .premium-background; +} + // Retina images // // Short retina mixin for setting background-image and -size diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less index c8bdc2a5f2..81477a4a44 100644 --- a/services/web/frontend/stylesheets/core/variables.less +++ b/services/web/frontend/stylesheets/core/variables.less @@ -38,6 +38,8 @@ @blue: #405ebf; @blue-10: #f1f4f9; @blue-30: #97b6e5; +@blue-40: #6597e0; +@blue-70: #214475; @blueDark: #040d2d; @green: #46a546; @green-10: #ebf6ea; @@ -1174,3 +1176,11 @@ @history-react-header-color: #fff; @history-react-separator-color: @neutral-80; @history-main-bg: #fff; + +// Gradients +@premium-gradient: linear-gradient( + 246deg, + @blue-70 0%, + #254c84 29%, + @blue-40 97% +); diff --git a/services/web/frontend/stylesheets/variables/all.less b/services/web/frontend/stylesheets/variables/all.less index 922052e9a3..a90fd03559 100644 --- a/services/web/frontend/stylesheets/variables/all.less +++ b/services/web/frontend/stylesheets/variables/all.less @@ -959,3 +959,11 @@ @history-react-header-color: #fff; @history-react-separator-color: @neutral-80; @history-main-bg: #fff; + +// Gradients +@premium-gradient: linear-gradient( + 246deg, + @blue-70 0%, + #254c84 29%, + @blue-40 97% +); diff --git a/services/web/test/frontend/features/history/components/change-list.spec.tsx b/services/web/test/frontend/features/history/components/change-list.spec.tsx index 7f5cfb7946..42e8674bd0 100644 --- a/services/web/test/frontend/features/history/components/change-list.spec.tsx +++ b/services/web/test/frontend/features/history/components/change-list.spec.tsx @@ -1,17 +1,22 @@ import { useState } from 'react' import ToggleSwitch from '../../../../../frontend/js/features/history/components/change-list/toggle-switch' import ChangeList from '../../../../../frontend/js/features/history/components/change-list/change-list' -import { EditorProviders } from '../../../helpers/editor-providers' +import { + EditorProviders, + USER_EMAIL, + USER_ID, +} from '../../../helpers/editor-providers' import { HistoryProvider } from '../../../../../frontend/js/features/history/context/history-context' import { updates } from '../fixtures/updates' import { labels } from '../fixtures/labels' const mountWithEditorProviders = ( component: React.ReactNode, - scope: Record = {} + scope: Record = {}, + props: Record = {} ) => { cy.mount( - +
    {component}
    @@ -79,7 +84,13 @@ describe('change list', function () { }) it('renders tags', function () { - mountWithEditorProviders(, scope) + mountWithEditorProviders(, scope, { + user: { + id: USER_ID, + email: USER_EMAIL, + isAdmin: true, + }, + }) waitForData() cy.findByLabelText(/all history/i).click({ force: true }) @@ -144,7 +155,13 @@ describe('change list', function () { }) it('deletes tag', function () { - mountWithEditorProviders(, scope) + mountWithEditorProviders(, scope, { + user: { + id: USER_ID, + email: USER_EMAIL, + isAdmin: true, + }, + }) waitForData() cy.findByLabelText(/all history/i).click({ force: true }) @@ -187,6 +204,8 @@ describe('change list', function () { cy.contains(/sorry, something went wrong/i) }) }) + cy.findByText(labelToDelete).should('have.length', 1) + cy.intercept('DELETE', '/project/*/labels/*', { statusCode: 204, }).as('delete') @@ -197,4 +216,189 @@ describe('change list', function () { cy.findByText(labelToDelete).should('not.exist') }) }) + + describe('paywall', function () { + const now = Date.now() + const oneMinuteAgo = now - 60 * 1000 + const justOverADayAgo = now - 25 * 60 * 60 * 1000 + const twoDaysAgo = now - 48 * 60 * 60 * 1000 + + const updates = { + updates: [ + { + fromV: 3, + toV: 4, + meta: { + users: [ + { + first_name: 'john.doe', + last_name: '', + email: 'john.doe@test.com', + id: '1', + }, + ], + start_ts: oneMinuteAgo, + end_ts: oneMinuteAgo, + }, + labels: [], + pathnames: [], + project_ops: [{ add: { pathname: 'name.tex' }, atV: 3 }], + }, + { + fromV: 1, + toV: 3, + meta: { + users: [ + { + first_name: 'bobby.lapointe', + last_name: '', + email: 'bobby.lapointe@test.com', + id: '2', + }, + ], + start_ts: justOverADayAgo, + end_ts: justOverADayAgo - 10 * 1000, + }, + labels: [], + pathnames: ['main.tex'], + project_ops: [], + }, + { + fromV: 0, + toV: 1, + meta: { + users: [ + { + first_name: 'john.doe', + last_name: '', + email: 'john.doe@test.com', + id: '1', + }, + ], + start_ts: twoDaysAgo, + end_ts: twoDaysAgo, + }, + labels: [ + { + id: 'label1', + comment: 'tag-1', + version: 0, + user_id: USER_ID, + created_at: justOverADayAgo, + }, + ], + pathnames: [], + project_ops: [{ add: { pathname: 'main.tex' }, atV: 0 }], + }, + ], + } + + const labels = [ + { + id: 'label1', + comment: 'tag-1', + version: 0, + user_id: USER_ID, + created_at: justOverADayAgo, + user_display_name: 'john.doe', + }, + ] + + const waitForData = () => { + cy.wait('@updates') + cy.wait('@labels') + cy.wait('@diff') + } + + beforeEach(function () { + cy.intercept('GET', '/project/*/updates*', { + body: updates, + }).as('updates') + cy.intercept('GET', '/project/*/labels', { + body: labels, + }).as('labels') + cy.intercept('GET', '/project/*/filetree/diff*', { + body: { diff: [{ pathname: 'main.tex' }, { pathname: 'name.tex' }] }, + }).as('diff') + }) + + it('shows non-owner paywall', function () { + const scope = { + ui: { + view: 'history', + pdfLayout: 'sideBySide', + chatOpen: true, + }, + } + + mountWithEditorProviders(, scope, { + user: { + id: USER_ID, + email: USER_EMAIL, + isAdmin: false, + }, + }) + + waitForData() + + cy.get('.history-paywall-prompt').should('have.length', 1) + cy.findAllByTestId('history-version').should('have.length', 2) + cy.get('.history-paywall-prompt button').should('not.exist') + }) + + it('shows owner paywall', function () { + const scope = { + ui: { + view: 'history', + pdfLayout: 'sideBySide', + chatOpen: true, + }, + } + + mountWithEditorProviders(, scope, { + user: { + id: USER_ID, + email: USER_EMAIL, + isAdmin: false, + }, + projectOwner: { + _id: USER_ID, + email: USER_EMAIL, + }, + }) + + waitForData() + + cy.get('.history-paywall-prompt').should('have.length', 1) + cy.findAllByTestId('history-version').should('have.length', 2) + cy.get('.history-paywall-prompt button').should('have.length', 1) + }) + + it('shows all labels in free tier', function () { + const scope = { + ui: { + view: 'history', + pdfLayout: 'sideBySide', + chatOpen: true, + }, + } + + mountWithEditorProviders(, scope, { + user: { + id: USER_ID, + email: USER_EMAIL, + isAdmin: false, + }, + projectOwner: { + _id: USER_ID, + email: USER_EMAIL, + }, + }) + + waitForData() + + cy.findByLabelText(/labels/i).click({ force: true }) + cy.get('.history-version-label').should('have.length', 1) + }) + }) }) diff --git a/services/web/test/frontend/helpers/editor-providers.js b/services/web/test/frontend/helpers/editor-providers.js index f221da50a7..68584b9b52 100644 --- a/services/web/test/frontend/helpers/editor-providers.js +++ b/services/web/test/frontend/helpers/editor-providers.js @@ -24,6 +24,10 @@ export const USER_EMAIL = 'testuser@example.com' export function EditorProviders({ user = { id: USER_ID, email: USER_EMAIL }, projectId = PROJECT_ID, + projectOwner = { + _id: '124abd', + email: 'owner@example.com', + }, rootDocId = '_root_doc_id', socket = { on: sinon.stub(), @@ -77,10 +81,7 @@ export function EditorProviders({ project: { _id: window.project_id, name: PROJECT_NAME, - owner: { - _id: '124abd', - email: 'owner@example.com', - }, + owner: projectOwner, features, rootDoc_id: rootDocId, rootFolder,