mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
817d1cffaf
commit
ebc04e4a9d
23 changed files with 750 additions and 48 deletions
|
@ -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
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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') + '…'}</>
|
||||
}
|
|
@ -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 && <ContactSupportToChangeGroupPlan />}
|
||||
{subscription.recurly.trial_ends_at &&
|
||||
{isInFreeTrial(subscription.recurly.trial_ends_at) &&
|
||||
subscription.recurly.trialEndsAtFormatted && (
|
||||
<TrialEnding
|
||||
trialEndsAt={subscription.recurly.trial_ends_at}
|
||||
trialEndsAtFormatted={subscription.recurly.trialEndsAtFormatted}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<p>
|
||||
<Trans
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
export const subscriptionUpdateUrl = '/user/subscription/update'
|
||||
export const cancelPendingSubscriptionChangeUrl =
|
||||
'/user/subscription/cancel-pending'
|
||||
export const cancelSubscriptionUrl = '/user/subscription/cancel'
|
||||
export const redirectAfterCancelSubscriptionUrl = '/user/subscription/canceled'
|
||||
export const extendTrialUrl = '/user/subscription/extend'
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export default function isMonthlyCollaboratorPlan(
|
||||
planCode: string,
|
||||
isGroupPlan?: boolean
|
||||
) {
|
||||
return (
|
||||
planCode.indexOf('collaborator') !== -1 &&
|
||||
planCode.indexOf('ann') === -1 &&
|
||||
!isGroupPlan
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -737,8 +737,7 @@
|
|||
"institutional_login_unknown": "Sorry, we don’t 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",
|
||||
"interested_in": "Interested in",
|
||||
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <strong>__price__</strong> Personal plan?",
|
||||
"invalid": "Invalid",
|
||||
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__</0> 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",
|
||||
|
|
|
@ -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('<ActiveSubscription />', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
|
@ -186,30 +193,220 @@ describe('<ActiveSubscription />', 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 () {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue