+}) {
+ 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__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",
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
+ })
+})