Merge pull request #11888 from overleaf/jel-subscription-dash-cancel-plan

[web] Migrate cancel subscription to React

GitOrigin-RevId: 8482fd61894c8011b4c980263ae1d41f396233c6
This commit is contained in:
Jessica Lawshe 2023-02-23 10:50:05 -06:00 committed by Copybot
parent 817d1cffaf
commit ebc04e4a9d
23 changed files with 750 additions and 48 deletions

View file

@ -102,7 +102,7 @@ div(ng-controller="RecurlySubscriptionController")
div(ng-show="showDowngrade") div(ng-show="showDowngrade")
div(ng-controller="ChangePlanFormController") div(ng-controller="ChangePlanFormController")
p !{translate("interested_in_cheaper_personal_plan",{price:'{{personalDisplayPrice}}'})} p !{translate("interested_in_cheaper_personal_plan", {price:'{{personalDisplayPrice}}'}, ['strong'] )}
p p
button(type="submit", ng-click="downgradeToPaidPersonal()", ng-disabled='inflight').btn.btn-primary #{translate("yes_move_me_to_personal_plan")} button(type="submit", ng-click="downgradeToPaidPersonal()", ng-disabled='inflight').btn.btn-primary #{translate("yes_move_me_to_personal_plan")}
p p

View file

@ -73,6 +73,7 @@
"can_now_relink_dropbox": "", "can_now_relink_dropbox": "",
"cancel": "", "cancel": "",
"cancel_anytime": "", "cancel_anytime": "",
"cancel_my_account": "",
"cancel_your_subscription": "", "cancel_your_subscription": "",
"cannot_invite_non_user": "", "cannot_invite_non_user": "",
"cannot_invite_self": "", "cannot_invite_self": "",
@ -335,6 +336,7 @@
"group_plan_with_name_tooltip": "", "group_plan_with_name_tooltip": "",
"group_subscription": "", "group_subscription": "",
"have_an_extra_backup": "", "have_an_extra_backup": "",
"have_more_days_to_try": "",
"headers": "", "headers": "",
"help": "", "help": "",
"hide_document_preamble": "", "hide_document_preamble": "",
@ -363,8 +365,10 @@
"hotkey_toggle_track_changes": "", "hotkey_toggle_track_changes": "",
"hotkey_undo": "", "hotkey_undo": "",
"hotkeys": "", "hotkeys": "",
"i_want_to_stay": "",
"if_error_persists_try_relinking_provider": "", "if_error_persists_try_relinking_provider": "",
"ignore_validation_errors": "", "ignore_validation_errors": "",
"ill_take_it": "",
"import_from_github": "", "import_from_github": "",
"import_to_sharelatex": "", "import_to_sharelatex": "",
"imported_from_another_project_at_date": "", "imported_from_another_project_at_date": "",
@ -512,6 +516,7 @@
"no_projects": "", "no_projects": "",
"no_search_results": "", "no_search_results": "",
"no_symbols_found": "", "no_symbols_found": "",
"no_thanks_cancel_now": "",
"normal": "", "normal": "",
"normally_x_price_per_month": "", "normally_x_price_per_month": "",
"normally_x_price_per_year": "", "normally_x_price_per_year": "",

View file

@ -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') + '…'}</>
}

View file

@ -4,12 +4,13 @@ import { PriceExceptions } from '../../../shared/price-exceptions'
import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription' import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { CancelSubscriptionButton } from './cancel-subscription-button' 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 { PendingPlanChange } from './pending-plan-change'
import { TrialEnding } from './trial-ending' import { TrialEnding } from './trial-ending'
import { ChangePlan } from './change-plan/change-plan' import { ChangePlan } from './change-plan/change-plan'
import { PendingAdditionalLicenses } from './pending-additional-licenses' import { PendingAdditionalLicenses } from './pending-additional-licenses'
import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan' import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan'
import isInFreeTrial from '../../../../util/is-in-free-trial'
export function ActiveSubscription({ export function ActiveSubscription({
subscription, subscription,
@ -72,10 +73,9 @@ export function ActiveSubscription({
{(!subscription.pendingPlan || {(!subscription.pendingPlan ||
subscription.pendingPlan.name === subscription.plan.name) && subscription.pendingPlan.name === subscription.plan.name) &&
subscription.plan.groupPlan && <ContactSupportToChangeGroupPlan />} subscription.plan.groupPlan && <ContactSupportToChangeGroupPlan />}
{subscription.recurly.trial_ends_at && {isInFreeTrial(subscription.recurly.trial_ends_at) &&
subscription.recurly.trialEndsAtFormatted && ( subscription.recurly.trialEndsAtFormatted && (
<TrialEnding <TrialEnding
trialEndsAt={subscription.recurly.trial_ends_at}
trialEndsAtFormatted={subscription.recurly.trialEndsAtFormatted} trialEndsAtFormatted={subscription.recurly.trialEndsAtFormatted}
/> />
)} )}

View file

@ -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 (
<button
className={`btn ${buttonClass}`}
onClick={handleCancelSubscription}
disabled={isButtonDisabled}
>
<ActionButtonText
inflight={isSuccessCancel || isLoadingCancel}
buttonText={buttonText}
/>
</button>
)
}
function NotCancelOption({
isButtonDisabled,
isLoadingSecondaryAction,
isSuccessSecondaryAction,
showExtendFreeTrial,
runAsyncSecondaryAction,
}: {
isButtonDisabled: boolean
isLoadingSecondaryAction: boolean
isSuccessSecondaryAction: boolean
showExtendFreeTrial: boolean
runAsyncSecondaryAction: (promise: Promise<unknown>) => Promise<unknown>
}) {
const { t } = useTranslation()
const { setShowCancellation } = useSubscriptionDashboardContext()
if (showExtendFreeTrial) {
return (
<>
<p>
<Trans
i18nKey="have_more_days_to_try"
values={{
days: 14,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</p>
<p>
<ExtendTrialButton
isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
runAsyncSecondaryAction={runAsyncSecondaryAction}
/>
</p>
</>
)
}
function handleKeepPlan() {
setShowCancellation(false)
}
return (
<p>
<button
className="btn btn-secondary-info btn-secondary"
onClick={handleKeepPlan}
>
{t('i_want_to_stay')}
</button>
</p>
)
}
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 (
<div className="text-center">
<p>
<strong>{t('wed_love_you_to_stay')}</strong>
</p>
{(isErrorCancel || isErrorSecondaryAction) && <GenericErrorAlert />}
<NotCancelOption
showExtendFreeTrial={showExtendFreeTrial}
isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
runAsyncSecondaryAction={runAsyncSecondaryAction}
/>
<ConfirmCancelSubscriptionButton
buttonClass={confirmCancelButtonClass}
buttonText={confirmCancelButtonText}
isButtonDisabled={isButtonDisabled}
handleCancelSubscription={handleCancelSubscription}
isSuccessCancel={isSuccessCancel}
isLoadingCancel={isLoadingCancel}
/>
</div>
)
}

View file

@ -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<unknown>) => Promise<unknown>
}) {
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 (
<>
<button
className="btn btn-primary"
onClick={handleExtendTrial}
disabled={isButtonDisabled}
>
<ActionButtonText
inflight={isLoadingSecondaryAction || isSuccessSecondaryAction}
buttonText={buttonText}
/>
</button>
</>
)
}

View file

@ -1,15 +0,0 @@
import { useTranslation } from 'react-i18next'
export function CancelSubscription() {
const { t } = useTranslation()
return (
<div className="text-center">
<p>
<strong>{t('wed_love_you_to_stay')}</strong>
</p>
{/* todo: showExtendFreeTrial */}
{/* todo: showDowngrade */}
{/* todo: showBasicCancel */}
</div>
)
}

View file

@ -1,15 +1,10 @@
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
export function TrialEnding({ export function TrialEnding({
trialEndsAt,
trialEndsAtFormatted, trialEndsAtFormatted,
}: { }: {
trialEndsAt: string
trialEndsAtFormatted: string trialEndsAtFormatted: string
}) { }) {
const endDate = new Date(trialEndsAt)
if (endDate.getTime() < Date.now()) return null
return ( return (
<p> <p>
<Trans <Trans

View file

@ -1,3 +1,6 @@
export const subscriptionUpdateUrl = '/user/subscription/update' export const subscriptionUpdateUrl = '/user/subscription/update'
export const cancelPendingSubscriptionChangeUrl = export const cancelPendingSubscriptionChangeUrl =
'/user/subscription/cancel-pending' '/user/subscription/cancel-pending'
export const cancelSubscriptionUrl = '/user/subscription/cancel'
export const redirectAfterCancelSubscriptionUrl = '/user/subscription/canceled'
export const extendTrialUrl = '/user/subscription/extend'

View file

@ -0,0 +1,13 @@
import freeTrialExpiresUnderSevenDays from './free-trial-expires-under-seven-days'
import isMonthlyCollaboratorPlan from './is-monthly-collaborator-plan'
export default function canExtendTrial(
planCode: string,
isGroupPlan?: boolean,
trialEndsAt?: string | null
) {
return (
isMonthlyCollaboratorPlan(planCode, isGroupPlan) &&
freeTrialExpiresUnderSevenDays(trialEndsAt)
)
}

View file

@ -0,0 +1,11 @@
export default function freeTrialExpiresUnderSevenDays(
trialEndsAt?: string | null
) {
if (!trialEndsAt) return false
const sevenDaysTime = new Date()
sevenDaysTime.setDate(sevenDaysTime.getDate() + 7)
const freeTrialEndDate = new Date(trialEndsAt)
return new Date() < freeTrialEndDate && freeTrialEndDate < sevenDaysTime
}

View file

@ -0,0 +1,9 @@
export default function isInFreeTrial(trialEndsAt?: string | null) {
if (!trialEndsAt) return false
const endDate = new Date(trialEndsAt)
if (endDate.getTime() < Date.now()) return false
return true
}

View file

@ -0,0 +1,10 @@
export default function isMonthlyCollaboratorPlan(
planCode: string,
isGroupPlan?: boolean
) {
return (
planCode.indexOf('collaborator') !== -1 &&
planCode.indexOf('ann') === -1 &&
!isGroupPlan
)
}

View file

@ -0,0 +1,13 @@
import isInFreeTrial from './is-in-free-trial'
import isMonthlyCollaboratorPlan from './is-monthly-collaborator-plan'
export default function showDowngradeOption(
planCode: string,
isGroupPlan?: boolean,
trialEndsAt?: string | null
) {
return (
isMonthlyCollaboratorPlan(planCode, isGroupPlan) &&
!isInFreeTrial(trialEndsAt)
)
}

View file

@ -737,8 +737,7 @@
"institutional_login_unknown": "Sorry, we dont know which institution issued that email address. You can browse our <a href=\"__link__\">list of institutions</a> to find yours, or you can use one of the other options below.", "institutional_login_unknown": "Sorry, we dont know which institution issued that email address. You can browse our <a href=\"__link__\">list of institutions</a> to find yours, or you can use one of the other options below.",
"integrations": "Integrations", "integrations": "Integrations",
"interested_in": "Interested in", "interested_in": "Interested in",
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <strong>__price__</strong> Personal plan?", "interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__</0> Personal plan?",
"invalid": "Invalid",
"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_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_email": "An email address is invalid",
"invalid_file_name": "Invalid File Name", "invalid_file_name": "Invalid File Name",

View file

@ -1,18 +1,25 @@
import { expect } from 'chai' 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 * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription' import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { import {
annualActiveSubscription, annualActiveSubscription,
groupActiveSubscription, groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange, groupActiveSubscriptionWithPendingLicenseChange,
monthlyActiveCollaborator,
pendingSubscriptionChange, pendingSubscriptionChange,
trialCollaboratorSubscription,
trialSubscription, trialSubscription,
} from '../../../../fixtures/subscriptions' } from '../../../../fixtures/subscriptions'
import sinon from 'sinon' import sinon from 'sinon'
import { cleanUpContext } from '../../../../helpers/render-with-subscription-dash-context' import { cleanUpContext } from '../../../../helpers/render-with-subscription-dash-context'
import { renderActiveSubscription } from '../../../../helpers/render-active-subscription' import { renderActiveSubscription } from '../../../../helpers/render-active-subscription'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import fetchMock from 'fetch-mock'
import {
cancelSubscriptionUrl,
extendTrialUrl,
} from '../../../../../../../../frontend/js/features/subscription/data/subscription-url'
describe('<ActiveSubscription />', function () { describe('<ActiveSubscription />', function () {
let sendMBSpy: sinon.SinonSpy let sendMBSpy: sinon.SinonSpy
@ -186,30 +193,220 @@ describe('<ActiveSubscription />', function () {
) )
}) })
it('shows cancel UI and sends event', function () { describe('cancel plan', function () {
renderActiveSubscription(annualActiveSubscription) const locationStub = sinon.stub()
// before button clicked const reloadStub = sinon.stub()
screen.getByText( const originalLocation = window.location
'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)
const button = screen.getByRole('button', { beforeEach(function () {
name: 'Cancel Your Subscription', Object.defineProperty(window, 'location', {
value: { assign: locationStub, reload: reloadStub },
})
}) })
fireEvent.click(button)
expect(sendMBSpy).to.be.calledOnceWith(
'subscription-page-cancel-button-click'
)
screen.getByText('Wed 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('Wed 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 = 'Ill 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 () { describe('group plans', function () {

View file

@ -432,3 +432,90 @@ export const customSubscription: CustomSubscription = {
}, },
customAccount: true, 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',
},
}

View file

@ -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
})
})

View file

@ -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
})
})

View file

@ -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
})
})

View file

@ -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
})
})

View file

@ -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
})
})