diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug index 15ad1c66f9..edac675cde 100644 --- a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug @@ -102,7 +102,7 @@ div(ng-controller="RecurlySubscriptionController") div(ng-show="showDowngrade") div(ng-controller="ChangePlanFormController") - p !{translate("interested_in_cheaper_personal_plan",{price:'{{personalDisplayPrice}}'})} + p !{translate("interested_in_cheaper_personal_plan", {price:'{{personalDisplayPrice}}'}, ['strong'] )} p button(type="submit", ng-click="downgradeToPaidPersonal()", ng-disabled='inflight').btn.btn-primary #{translate("yes_move_me_to_personal_plan")} p diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index cfe2c47de2..ee8b443723 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -73,6 +73,7 @@ "can_now_relink_dropbox": "", "cancel": "", "cancel_anytime": "", + "cancel_my_account": "", "cancel_your_subscription": "", "cannot_invite_non_user": "", "cannot_invite_self": "", @@ -335,6 +336,7 @@ "group_plan_with_name_tooltip": "", "group_subscription": "", "have_an_extra_backup": "", + "have_more_days_to_try": "", "headers": "", "help": "", "hide_document_preamble": "", @@ -363,8 +365,10 @@ "hotkey_toggle_track_changes": "", "hotkey_undo": "", "hotkeys": "", + "i_want_to_stay": "", "if_error_persists_try_relinking_provider": "", "ignore_validation_errors": "", + "ill_take_it": "", "import_from_github": "", "import_to_sharelatex": "", "imported_from_another_project_at_date": "", @@ -512,6 +516,7 @@ "no_projects": "", "no_search_results": "", "no_symbols_found": "", + "no_thanks_cancel_now": "", "normal": "", "normally_x_price_per_month": "", "normally_x_price_per_year": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/action-button-text.tsx b/services/web/frontend/js/features/subscription/components/dashboard/action-button-text.tsx new file mode 100644 index 0000000000..805306f84e --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/action-button-text.tsx @@ -0,0 +1,12 @@ +import { useTranslation } from 'react-i18next' + +export default function ActionButtonText({ + inflight, + buttonText, +}: { + inflight: boolean + buttonText: string +}) { + const { t } = useTranslation() + return <>{!inflight ? buttonText : t('processing_uppercase') + '…'} +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx index 1e4683a45d..a36542d01e 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx @@ -4,12 +4,13 @@ import { PriceExceptions } from '../../../shared/price-exceptions' import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription' import { CancelSubscriptionButton } from './cancel-subscription-button' -import { CancelSubscription } from './cancel-subscription' +import { CancelSubscription } from './cancel-plan/cancel-subscription' import { PendingPlanChange } from './pending-plan-change' import { TrialEnding } from './trial-ending' import { ChangePlan } from './change-plan/change-plan' import { PendingAdditionalLicenses } from './pending-additional-licenses' import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan' +import isInFreeTrial from '../../../../util/is-in-free-trial' export function ActiveSubscription({ subscription, @@ -72,10 +73,9 @@ export function ActiveSubscription({ {(!subscription.pendingPlan || subscription.pendingPlan.name === subscription.plan.name) && subscription.plan.groupPlan && } - {subscription.recurly.trial_ends_at && + {isInFreeTrial(subscription.recurly.trial_ends_at) && subscription.recurly.trialEndsAtFormatted && ( )} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx new file mode 100644 index 0000000000..20e708ed1e --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx @@ -0,0 +1,183 @@ +import { Trans, useTranslation } from 'react-i18next' +import { postJSON } from '../../../../../../../infrastructure/fetch-json' +import useAsync from '../../../../../../../shared/hooks/use-async' +import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context' +import { + cancelSubscriptionUrl, + redirectAfterCancelSubscriptionUrl, +} from '../../../../../data/subscription-url' +import canExtendTrial from '../../../../../util/can-extend-trial' +import showDowngradeOption from '../../../../../util/show-downgrade-option' +import ActionButtonText from '../../../action-button-text' +import GenericErrorAlert from '../../../generic-error-alert' +import ExtendTrialButton from './extend-trial-button' + +function ConfirmCancelSubscriptionButton({ + buttonClass, + buttonText, + handleCancelSubscription, + isLoadingCancel, + isSuccessCancel, + isButtonDisabled, +}: { + buttonClass: string + buttonText: string + handleCancelSubscription: () => void + isLoadingCancel: boolean + isSuccessCancel: boolean + isButtonDisabled: boolean +}) { + return ( + + ) +} + +function NotCancelOption({ + isButtonDisabled, + isLoadingSecondaryAction, + isSuccessSecondaryAction, + showExtendFreeTrial, + runAsyncSecondaryAction, +}: { + isButtonDisabled: boolean + isLoadingSecondaryAction: boolean + isSuccessSecondaryAction: boolean + showExtendFreeTrial: boolean + runAsyncSecondaryAction: (promise: Promise) => Promise +}) { + const { t } = useTranslation() + + const { setShowCancellation } = useSubscriptionDashboardContext() + + if (showExtendFreeTrial) { + return ( + <> +

+ , + ]} + /> +

+

+ +

+ + ) + } + + function handleKeepPlan() { + setShowCancellation(false) + } + + return ( +

+ +

+ ) +} + +export function CancelSubscription() { + const { t } = useTranslation() + const { personalSubscription } = useSubscriptionDashboardContext() + + const { + isLoading: isLoadingCancel, + isError: isErrorCancel, + isSuccess: isSuccessCancel, + runAsync: runAsyncCancel, + } = useAsync() + const { + isLoading: isLoadingSecondaryAction, + isError: isErrorSecondaryAction, + isSuccess: isSuccessSecondaryAction, + runAsync: runAsyncSecondaryAction, + } = useAsync() + + const isButtonDisabled = + isLoadingCancel || + isLoadingSecondaryAction || + isSuccessSecondaryAction || + isSuccessCancel + + if (!personalSubscription || !('recurly' in personalSubscription)) return null + + async function handleCancelSubscription() { + try { + await runAsyncCancel(postJSON(cancelSubscriptionUrl)) + window.location.assign(redirectAfterCancelSubscriptionUrl) + } catch (e) { + console.error(e) + } + } + + const showExtendFreeTrial = canExtendTrial( + personalSubscription.plan.planCode, + personalSubscription.plan.groupPlan, + personalSubscription.recurly.trial_ends_at + ) + + const showDowngrade = showDowngradeOption( + personalSubscription.plan.planCode, + personalSubscription.plan.groupPlan, + personalSubscription.recurly.trial_ends_at + ) + + let confirmCancelButtonText = t('cancel_my_account') + let confirmCancelButtonClass = 'btn-primary' + if (showExtendFreeTrial || showDowngrade) { + confirmCancelButtonText = t('no_thanks_cancel_now') + confirmCancelButtonClass = 'btn-inline-link' + } + + return ( +
+

+ {t('wed_love_you_to_stay')} +

+ + {(isErrorCancel || isErrorSecondaryAction) && } + + + + +
+ ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx new file mode 100644 index 0000000000..080d783d78 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next' +import { putJSON } from '../../../../../../../infrastructure/fetch-json' +import { extendTrialUrl } from '../../../../../data/subscription-url' +import ActionButtonText from '../../../action-button-text' + +export default function ExtendTrialButton({ + isButtonDisabled, + isLoadingSecondaryAction, + isSuccessSecondaryAction, + runAsyncSecondaryAction, +}: { + isButtonDisabled: boolean + isLoadingSecondaryAction: boolean + isSuccessSecondaryAction: boolean + runAsyncSecondaryAction: (promise: Promise) => Promise +}) { + const { t } = useTranslation() + const buttonText = t('ill_take_it') + + async function handleExtendTrial() { + try { + await runAsyncSecondaryAction(putJSON(extendTrialUrl)) + window.location.reload() + } catch (e) { + console.error(e) + } + } + + return ( + <> + + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx deleted file mode 100644 index eb4ed9d4a9..0000000000 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useTranslation } from 'react-i18next' - -export function CancelSubscription() { - const { t } = useTranslation() - return ( -
-

- {t('wed_love_you_to_stay')} -

- {/* todo: showExtendFreeTrial */} - {/* todo: showDowngrade */} - {/* todo: showBasicCancel */} -
- ) -} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx index ebb69a8272..8e031e7454 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx @@ -1,15 +1,10 @@ import { Trans } from 'react-i18next' export function TrialEnding({ - trialEndsAt, trialEndsAtFormatted, }: { - trialEndsAt: string trialEndsAtFormatted: string }) { - const endDate = new Date(trialEndsAt) - if (endDate.getTime() < Date.now()) return null - return (

list of institutions to find yours, or you can use one of the other options below.", "integrations": "Integrations", "interested_in": "Interested in", - "interested_in_cheaper_personal_plan": "Would you be interested in the cheaper __price__ Personal plan?", - "invalid": "Invalid", + "interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__ Personal plan?", "invalid_element_name": "Could not copy your project because of filenames containing invalid characters\r\n(such as asterisks, slashes or control characters). Please rename the files and\r\ntry again.", "invalid_email": "An email address is invalid", "invalid_file_name": "Invalid File Name", diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 4dc4c157b1..29be349be3 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -1,18 +1,25 @@ import { expect } from 'chai' -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking' import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription' import { annualActiveSubscription, groupActiveSubscription, groupActiveSubscriptionWithPendingLicenseChange, + monthlyActiveCollaborator, pendingSubscriptionChange, + trialCollaboratorSubscription, trialSubscription, } from '../../../../fixtures/subscriptions' import sinon from 'sinon' import { cleanUpContext } from '../../../../helpers/render-with-subscription-dash-context' import { renderActiveSubscription } from '../../../../helpers/render-active-subscription' import { cloneDeep } from 'lodash' +import fetchMock from 'fetch-mock' +import { + cancelSubscriptionUrl, + extendTrialUrl, +} from '../../../../../../../../frontend/js/features/subscription/data/subscription-url' describe('', function () { let sendMBSpy: sinon.SinonSpy @@ -186,30 +193,220 @@ describe('', function () { ) }) - it('shows cancel UI and sends event', function () { - renderActiveSubscription(annualActiveSubscription) - // before button clicked - screen.getByText( - 'Your subscription will remain active until the end of your billing period', - { exact: false } - ) - const dates = screen.getAllByText( - annualActiveSubscription.recurly.nextPaymentDueAt, - { - exact: false, - } - ) - expect(dates.length).to.equal(2) + describe('cancel plan', function () { + const locationStub = sinon.stub() + const reloadStub = sinon.stub() + const originalLocation = window.location - const button = screen.getByRole('button', { - name: 'Cancel Your Subscription', + beforeEach(function () { + Object.defineProperty(window, 'location', { + value: { assign: locationStub, reload: reloadStub }, + }) }) - fireEvent.click(button) - expect(sendMBSpy).to.be.calledOnceWith( - 'subscription-page-cancel-button-click' - ) - screen.getByText('We’d love you to stay') + afterEach(function () { + Object.defineProperty(window, 'location', { + value: originalLocation, + }) + fetchMock.reset() + }) + + function showConfirmCancelUI() { + const button = screen.getByRole('button', { + name: 'Cancel Your Subscription', + }) + fireEvent.click(button) + } + + it('shows cancel UI and sends event', function () { + renderActiveSubscription(annualActiveSubscription) + // before button clicked + screen.getByText( + 'Your subscription will remain active until the end of your billing period', + { exact: false } + ) + const dates = screen.getAllByText( + annualActiveSubscription.recurly.nextPaymentDueAt, + { + exact: false, + } + ) + expect(dates.length).to.equal(2) + + showConfirmCancelUI() + + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-cancel-button-click' + ) + + screen.getByText('We’d love you to stay') + screen.getByRole('button', { name: 'Cancel my subscription' }) + }) + + it('cancels subscription and redirects page', async function () { + const endPointResponse = { + status: 200, + } + fetchMock.post(cancelSubscriptionUrl, endPointResponse) + renderActiveSubscription(annualActiveSubscription) + showConfirmCancelUI() + const button = screen.getByRole('button', { + name: 'Cancel my subscription', + }) + fireEvent.click(button) + await waitFor(() => { + expect(locationStub).to.have.been.called + }) + sinon.assert.calledWithMatch(locationStub, '/user/subscription/canceled') + }) + + it('shows an error message if canceling subscription failed', async function () { + const endPointResponse = { + status: 500, + } + fetchMock.post(cancelSubscriptionUrl, endPointResponse) + renderActiveSubscription(annualActiveSubscription) + showConfirmCancelUI() + const button = screen.getByRole('button', { + name: 'Cancel my subscription', + }) + fireEvent.click(button) + await screen.findByText('Sorry, something went wrong. ', { + exact: false, + }) + screen.getByText('Please try again. ', { exact: false }) + screen.getByText('If the problem continues please contact us.', { + exact: false, + }) + }) + + it('disables cancels subscription button after clicking and updates text', async function () { + renderActiveSubscription(annualActiveSubscription) + showConfirmCancelUI() + screen.getByRole('button', { + name: 'I want to stay', + }) + const button = screen.getByRole('button', { + name: 'Cancel my subscription', + }) + fireEvent.click(button) + + const cancelButtton = screen.getByRole('button', { + name: 'Processing…', + }) as HTMLButtonElement + expect(cancelButtton.disabled).to.be.true + + expect(screen.queryByText('Cancel my subscription')).to.be.null + }) + + describe('extend trial', function () { + const cancelButtonText = 'No thanks, I still want to cancel' + const extendTrialButtonText = 'I’ll take it!' + it('shows alternate cancel subscription button text for cancel button and option to extend trial', function () { + renderActiveSubscription(trialCollaboratorSubscription) + showConfirmCancelUI() + screen.getByText('Have another', { exact: false }) + screen.getByText('14 days', { exact: false }) + screen.getByText('on your Trial!', { exact: false }) + screen.getByRole('button', { + name: cancelButtonText, + }) + screen.getByRole('button', { + name: extendTrialButtonText, + }) + }) + + it('disables both buttons and updates text for when trial button clicked', function () { + renderActiveSubscription(trialCollaboratorSubscription) + showConfirmCancelUI() + const extendTrialButton = screen.getByRole('button', { + name: extendTrialButtonText, + }) + fireEvent.click(extendTrialButton) + + const buttons = screen.getAllByRole('button') + expect(buttons.length).to.equal(2) + expect(buttons[0].getAttribute('disabled')).to.equal('') + expect(buttons[1].getAttribute('disabled')).to.equal('') + screen.getByRole('button', { + name: cancelButtonText, + }) + screen.getByRole('button', { + name: 'Processing…', + }) + }) + + it('disables both buttons and updates text for when cancel button clicked', function () { + renderActiveSubscription(trialCollaboratorSubscription) + showConfirmCancelUI() + const cancelButtton = screen.getByRole('button', { + name: cancelButtonText, + }) + fireEvent.click(cancelButtton) + + const buttons = screen.getAllByRole('button') + expect(buttons.length).to.equal(2) + expect(buttons[0].getAttribute('disabled')).to.equal('') + expect(buttons[1].getAttribute('disabled')).to.equal('') + screen.getByRole('button', { + name: 'Processing…', + }) + screen.getByRole('button', { + name: extendTrialButtonText, + }) + }) + + it('does not show option to extend trial when not a collaborator trial', function () { + const trialPlan = cloneDeep(trialCollaboratorSubscription) + trialPlan.plan.planCode = 'anotherplan' + renderActiveSubscription(trialPlan) + showConfirmCancelUI() + expect( + screen.queryByRole('button', { + name: extendTrialButtonText, + }) + ).to.be.null + }) + + it('does not show option to extend trial when a collaborator trial but does not expire in 7 days', function () { + const trialPlan = cloneDeep(trialCollaboratorSubscription) + trialPlan.recurly.trial_ends_at = null + renderActiveSubscription(trialPlan) + showConfirmCancelUI() + expect( + screen.queryByRole('button', { + name: extendTrialButtonText, + }) + ).to.be.null + }) + + it('reloads page after the succesful request to extend trial', async function () { + const endPointResponse = { + status: 200, + } + fetchMock.put(extendTrialUrl, endPointResponse) + renderActiveSubscription(trialCollaboratorSubscription) + showConfirmCancelUI() + const extendTrialButton = screen.getByRole('button', { + name: extendTrialButtonText, + }) + fireEvent.click(extendTrialButton) + // page is reloaded on success + await waitFor(() => { + expect(reloadStub).to.have.been.called + }) + }) + }) + + describe('downgrade plan', function () { + it('shows alternate cancel subscription button text', function () { + renderActiveSubscription(monthlyActiveCollaborator) + showConfirmCancelUI() + screen.getByRole('button', { + name: 'No thanks, I still want to cancel', + }) + }) + }) }) describe('group plans', function () { diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx index 3d46da083f..2229c8a753 100644 --- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx @@ -432,3 +432,90 @@ export const customSubscription: CustomSubscription = { }, customAccount: true, } + +export const trialCollaboratorSubscription: RecurlySubscription = { + manager_ids: ['abc123'], + member_ids: [], + invited_emails: [], + groupPlan: false, + membersLimit: 0, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'collaborator_free_trial_7_days', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'collaborator_free_trial_7_days', + name: 'Standard (Collaborator)', + price_in_cents: 2300, + featureDescription: [], + hideFromUsers: true, + }, + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 0, + totalLicenses: 0, + nextPaymentDueAt: sevenDaysFromTodayFormatted, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: sevenDaysFromTodayFormatted, + trial_ends_at: new Date(sevenDaysFromToday).toString(), + activeCoupons: [], + account: { + has_canceled_subscription: { + _: 'false', + $: { + type: 'boolean', + }, + }, + has_past_due_invoice: { + _: 'false', + $: { + type: 'boolean', + }, + }, + }, + displayPrice: '$21.00', + }, +} + +export const monthlyActiveCollaborator: RecurlySubscription = { + manager_ids: ['abc123'], + member_ids: [], + invited_emails: [], + groupPlan: false, + membersLimit: 0, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'collaborator', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'collaborator', + name: 'Standard (Collaborator)', + price_in_cents: 212300900, + featureDescription: [], + }, + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 0, + totalLicenses: 0, + nextPaymentDueAt, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: null, + trial_ends_at: null, + activeCoupons: [], + account: { + has_canceled_subscription: { _: 'false', $: { type: 'boolean' } }, + has_past_due_invoice: { _: 'false', $: { type: 'boolean' } }, + }, + displayPrice: '$21.00', + }, +} diff --git a/services/web/test/frontend/features/subscription/util/can-extend-trial.test.ts b/services/web/test/frontend/features/subscription/util/can-extend-trial.test.ts new file mode 100644 index 0000000000..1799f01994 --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/can-extend-trial.test.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai' +import canExtendTrial from '../../../../../frontend/js/features/subscription/util/can-extend-trial' + +describe('canExtendTrial', function () { + const d = new Date() + d.setDate(d.getDate() + 6) + + it('returns false when no trial end date', function () { + expect(canExtendTrial('collab')).to.be.false + }) + it('returns false when a plan code without "collaborator" ', function () { + expect(canExtendTrial('test', false, d.toString())).to.be.false + }) + it('returns false when on a plan with trial date in future but has "collaborator" and "ann" in plan code', function () { + expect(canExtendTrial('collaborator-annual', false, d.toString())).to.be + .false + }) + it('returns false when on a plan with trial date in future and plan code has "collaborator" and no "ann" but is a group plan', function () { + expect(canExtendTrial('collaborator', true, d.toString())).to.be.false + }) + it('returns true when on a plan with "collaborator" and without "ann" and trial date in future', function () { + expect(canExtendTrial('collaborator', false, d.toString())).to.be.true + }) +}) diff --git a/services/web/test/frontend/features/subscription/util/free-trial-expires-under-seven-days.test.ts b/services/web/test/frontend/features/subscription/util/free-trial-expires-under-seven-days.test.ts new file mode 100644 index 0000000000..744619aeca --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/free-trial-expires-under-seven-days.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai' +import freeTrialExpiresUnderSevenDays from '../../../../../frontend/js/features/subscription/util/free-trial-expires-under-seven-days' + +describe('freeTrialExpiresUnderSevenDays', function () { + it('returns false when no date sent', function () { + expect(freeTrialExpiresUnderSevenDays()).to.be.false + }) + it('returns false when date is null', function () { + expect(freeTrialExpiresUnderSevenDays(null)).to.be.false + }) + it('returns false when date is in the past', function () { + expect(freeTrialExpiresUnderSevenDays('2000-02-16T17:59:07.000Z')).to.be + .false + }) + it('returns true when date is in 6 days', function () { + const d = new Date() + d.setDate(d.getDate() + 6) + expect(freeTrialExpiresUnderSevenDays(d.toString())).to.be.true + }) + it('returns false when date is in 8 days', function () { + const d = new Date() + d.setDate(d.getDate() + 8) + expect(freeTrialExpiresUnderSevenDays(d.toString())).to.be.false + }) +}) diff --git a/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts b/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts new file mode 100644 index 0000000000..e634c1d7cd --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai' +import isInFreeTrial from '../../../../../frontend/js/features/subscription/util/is-in-free-trial' +const dateformat = require('dateformat') + +describe('isInFreeTrial', function () { + it('returns false when no date sent', function () { + expect(isInFreeTrial()).to.be.false + }) + it('returns false when date is null', function () { + expect(isInFreeTrial(null)).to.be.false + }) + it('returns false when date is in the past', function () { + expect(isInFreeTrial('2000-02-16T17:59:07.000Z')).to.be.false + }) + it('returns true when date is in the future', function () { + const today = new Date() + const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) + const sevenDaysFromTodayFormatted = dateformat( + sevenDaysFromToday, + 'dS mmmm yyyy' + ) + expect(isInFreeTrial(sevenDaysFromTodayFormatted)).to.be.true + }) +}) diff --git a/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts b/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts new file mode 100644 index 0000000000..dd9c2a64b1 --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import isMonthlyCollaboratorPlan from '../../../../../frontend/js/features/subscription/util/is-monthly-collaborator-plan' + +describe('isMonthlyCollaboratorPlan', function () { + it('returns false when a plan code without "collaborator" ', function () { + expect(isMonthlyCollaboratorPlan('test', false)).to.be.false + }) + it('returns false when on a plan with "collaborator" and "ann"', function () { + expect(isMonthlyCollaboratorPlan('collaborator-annual', false)).to.be.false + }) + it('returns false when on a plan with "collaborator" and without "ann" but is a group plan', function () { + expect(isMonthlyCollaboratorPlan('collaborator', true)).to.be.false + }) + it('returns true when on a plan with non-group "collaborator" monthly plan', function () { + expect(isMonthlyCollaboratorPlan('collaborator', false)).to.be.true + }) +}) diff --git a/services/web/test/frontend/features/subscription/util/recurly-pricing.test.tsx b/services/web/test/frontend/features/subscription/util/recurly-pricing.test.ts similarity index 100% rename from services/web/test/frontend/features/subscription/util/recurly-pricing.test.tsx rename to services/web/test/frontend/features/subscription/util/recurly-pricing.test.ts diff --git a/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts b/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts new file mode 100644 index 0000000000..ac50319af6 --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai' +import showDowngradeOption from '../../../../../frontend/js/features/subscription/util/show-downgrade-option' +const dateformat = require('dateformat') + +describe('showDowngradeOption', function () { + const today = new Date() + const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) + const sevenDaysFromTodayFormatted = dateformat( + sevenDaysFromToday, + 'dS mmmm yyyy' + ) + + it('returns false when no trial end date', function () { + expect(showDowngradeOption('collab')).to.be.false + }) + it('returns false when a plan code without "collaborator" ', function () { + expect(showDowngradeOption('test', false, sevenDaysFromTodayFormatted)).to + .be.false + }) + it('returns false when on a plan with trial date in future but has "collaborator" and "ann" in plan code', function () { + expect( + showDowngradeOption( + 'collaborator-annual', + false, + sevenDaysFromTodayFormatted + ) + ).to.be.false + }) + it('returns false when on a plan with trial date in future and plan code has "collaborator" and no "ann" but is a group plan', function () { + expect( + showDowngradeOption('collaborator', true, sevenDaysFromTodayFormatted) + ).to.be.false + }) + it('returns false when on a plan with "collaborator" and without "ann" and trial date in future', function () { + expect( + showDowngradeOption('collaborator', false, sevenDaysFromTodayFormatted) + ).to.be.false + }) + it('returns true when on a plan with "collaborator" and without "ann" and no trial date', function () { + expect(showDowngradeOption('collaborator', false)).to.be.true + }) + it('returns true when on a plan with "collaborator" and without "ann" and trial date is in the past', function () { + expect( + showDowngradeOption('collaborator', false, '2000-02-16T17:59:07.000Z') + ).to.be.true + }) +})