mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #11681 from overleaf/jel-confirm-subscription-change
[web] Plan change request in subscription dash GitOrigin-RevId: 9afe22b2da035f887c6fa9abe8129711d492108c
This commit is contained in:
parent
4dcdd2e07a
commit
d539aaf226
11 changed files with 523 additions and 75 deletions
|
@ -224,6 +224,7 @@
|
||||||
"error": "",
|
"error": "",
|
||||||
"error_performing_request": "",
|
"error_performing_request": "",
|
||||||
"example_project": "",
|
"example_project": "",
|
||||||
|
"existing_plan_active_until_term_end": "",
|
||||||
"expand": "",
|
"expand": "",
|
||||||
"export_csv": "",
|
"export_csv": "",
|
||||||
"export_project_to_github": "",
|
"export_project_to_github": "",
|
||||||
|
@ -286,6 +287,7 @@
|
||||||
"galileo_suggestion_feedback_button": "",
|
"galileo_suggestion_feedback_button": "",
|
||||||
"galileo_suggestions_loading_error": "",
|
"galileo_suggestions_loading_error": "",
|
||||||
"galileo_toggle_description": "",
|
"galileo_toggle_description": "",
|
||||||
|
"generic_if_problem_continues_contact_us": "",
|
||||||
"generic_linked_file_compile_error": "",
|
"generic_linked_file_compile_error": "",
|
||||||
"generic_something_went_wrong": "",
|
"generic_something_went_wrong": "",
|
||||||
"get_collaborative_benefits": "",
|
"get_collaborative_benefits": "",
|
||||||
|
@ -566,6 +568,7 @@
|
||||||
"proceed_to_paypal": "",
|
"proceed_to_paypal": "",
|
||||||
"proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay": "",
|
"proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay": "",
|
||||||
"processing": "",
|
"processing": "",
|
||||||
|
"processing_uppercase": "",
|
||||||
"professional": "",
|
"professional": "",
|
||||||
"project": "",
|
"project": "",
|
||||||
"project_approaching_file_limit": "",
|
"project_approaching_file_limit": "",
|
||||||
|
@ -633,6 +636,7 @@
|
||||||
"resend_confirmation_email": "",
|
"resend_confirmation_email": "",
|
||||||
"resending_confirmation_email": "",
|
"resending_confirmation_email": "",
|
||||||
"reverse_x_sort_order": "",
|
"reverse_x_sort_order": "",
|
||||||
|
"revert_pending_plan_change": "",
|
||||||
"review": "",
|
"review": "",
|
||||||
"revoke": "",
|
"revoke": "",
|
||||||
"revoke_invite": "",
|
"revoke_invite": "",
|
||||||
|
@ -733,6 +737,8 @@
|
||||||
"subscription_admins_cannot_be_deleted": "",
|
"subscription_admins_cannot_be_deleted": "",
|
||||||
"subscription_canceled_and_terminate_on_x": "",
|
"subscription_canceled_and_terminate_on_x": "",
|
||||||
"subscription_will_remain_active_until_end_of_billing_period_x": "",
|
"subscription_will_remain_active_until_end_of_billing_period_x": "",
|
||||||
|
"sure_you_want_to_cancel_plan_change": "",
|
||||||
|
"sure_you_want_to_change_plan": "",
|
||||||
"sure_you_want_to_delete": "",
|
"sure_you_want_to_delete": "",
|
||||||
"switch_to_editor": "",
|
"switch_to_editor": "",
|
||||||
"switch_to_pdf": "",
|
"switch_to_pdf": "",
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { useTranslation } from 'react-i18next'
|
||||||
import LoadingSpinner from '../../../../../../../shared/components/loading-spinner'
|
import LoadingSpinner from '../../../../../../../shared/components/loading-spinner'
|
||||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||||
import { ChangeToGroupPlan } from './change-to-group-plan'
|
import { ChangeToGroupPlan } from './change-to-group-plan'
|
||||||
|
import { ConfirmChangePlanModal } from './confirm-change-plan-modal'
|
||||||
import { IndividualPlansTable } from './individual-plans-table'
|
import { IndividualPlansTable } from './individual-plans-table'
|
||||||
|
import { KeepCurrentPlanModal } from './keep-current-plan-modal'
|
||||||
|
|
||||||
export function ChangePlan() {
|
export function ChangePlan() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -28,6 +30,8 @@ export function ChangePlan() {
|
||||||
<h2>{t('change_plan')}</h2>
|
<h2>{t('change_plan')}</h2>
|
||||||
<IndividualPlansTable plans={plans} />
|
<IndividualPlansTable plans={plans} />
|
||||||
<ChangeToGroupPlan />
|
<ChangeToGroupPlan />
|
||||||
|
<ConfirmChangePlanModal />
|
||||||
|
<KeepCurrentPlanModal />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Modal } from 'react-bootstrap'
|
||||||
|
import { useTranslation, Trans } from 'react-i18next'
|
||||||
|
import { postJSON } from '../../../../../../../infrastructure/fetch-json'
|
||||||
|
import AccessibleModal from '../../../../../../../shared/components/accessible-modal'
|
||||||
|
import getMeta from '../../../../../../../utils/meta'
|
||||||
|
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||||
|
import { subscriptionUrl } from '../../../../../data/subscription-url'
|
||||||
|
|
||||||
|
export function ConfirmChangePlanModal() {
|
||||||
|
const modalId = 'change-to-plan'
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [inflight, setInflight] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleCloseModal, modalIdShown, plans, planCodeToChangeTo } =
|
||||||
|
useSubscriptionDashboardContext()
|
||||||
|
const planCodesChangingAtTermEnd = getMeta('ol-planCodesChangingAtTermEnd')
|
||||||
|
|
||||||
|
async function handleConfirmChange() {
|
||||||
|
setError(false)
|
||||||
|
setInflight(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postJSON(`${subscriptionUrl}?origin=confirmChangePlan`, {
|
||||||
|
body: {
|
||||||
|
plan_code: planCodeToChangeTo,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
setError(true)
|
||||||
|
setInflight(false)
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalIdShown !== modalId || !planCodeToChangeTo) return null
|
||||||
|
|
||||||
|
const plan = plans.find(p => p.planCode === planCodeToChangeTo)
|
||||||
|
if (!plan) return null
|
||||||
|
|
||||||
|
const planWillChangeAtTermEnd =
|
||||||
|
planCodesChangingAtTermEnd?.indexOf(planCodeToChangeTo) > -1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleModal
|
||||||
|
id={modalId}
|
||||||
|
show
|
||||||
|
animation
|
||||||
|
onHide={handleCloseModal}
|
||||||
|
backdrop="static"
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>{t('change_plan')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<Modal.Body>
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-warning">
|
||||||
|
{t('generic_something_went_wrong')}. {t('try_again')}.{' '}
|
||||||
|
{t('generic_if_problem_continues_contact_us')}.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="sure_you_want_to_change_plan"
|
||||||
|
values={{
|
||||||
|
planName: plan.name,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
|
<strong />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{planWillChangeAtTermEnd && (
|
||||||
|
<>
|
||||||
|
<p>{t('existing_plan_active_until_term_end')}</p>
|
||||||
|
<p>{t('want_change_to_apply_before_plan_end')}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Footer>
|
||||||
|
<button
|
||||||
|
disabled={inflight}
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={inflight}
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleConfirmChange}
|
||||||
|
>
|
||||||
|
{!inflight ? t('change_plan') : t('processing_uppercase') + '…'}
|
||||||
|
</button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AccessibleModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,37 +3,33 @@ import { Plan } from '../../../../../../../../../types/subscription/plan'
|
||||||
import Icon from '../../../../../../../shared/components/icon'
|
import Icon from '../../../../../../../shared/components/icon'
|
||||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||||
|
|
||||||
function ChangeToPlanButton({ plan }: { plan: Plan }) {
|
function ChangeToPlanButton({ planCode }: { planCode: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
// for when the user selected to change a plan, but the plan change is still pending
|
const { handleOpenModal } = useSubscriptionDashboardContext()
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleOpenModal('change-to-plan', planCode)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form>
|
<button className="btn btn-primary" onClick={handleClick}>
|
||||||
{/* todo: ng-model="plan_code" */}
|
{t('change_to_this_plan')}
|
||||||
<input type="hidden" name="plan_code" value={plan.planCode} />
|
</button>
|
||||||
{/* todo: handle submit changePlan */}
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
value={t('change_to_this_plan')}
|
|
||||||
className="btn btn-primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function KeepCurrentPlanButton({ plan }: { plan: Plan }) {
|
function KeepCurrentPlanButton({ plan }: { plan: Plan }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
// for when the user selected to change a plan, but the plan change is still pending
|
const { handleOpenModal } = useSubscriptionDashboardContext()
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleOpenModal('keep-current-plan')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form>
|
<button className="btn btn-primary" onClick={handleClick}>
|
||||||
{/* todo: ng-model="plan_code" */}
|
{t('keep_current_plan')}
|
||||||
<input type="hidden" name="plan_code" value={plan.planCode} />
|
</button>
|
||||||
{/* todo: handle submit cancelPendingPlanChange */}
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
value={t('keep_current_plan')}
|
|
||||||
className="btn btn-primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +57,7 @@ function ChangePlanButton({ plan }: { plan: Plan }) {
|
||||||
</b>
|
</b>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <ChangeToPlanButton plan={plan} />
|
return <ChangeToPlanButton planCode={plan.planCode} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Modal } from 'react-bootstrap'
|
||||||
|
import { useTranslation, Trans } from 'react-i18next'
|
||||||
|
import { postJSON } from '../../../../../../../infrastructure/fetch-json'
|
||||||
|
import AccessibleModal from '../../../../../../../shared/components/accessible-modal'
|
||||||
|
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||||
|
import { cancelPendingSubscriptionChangeUrl } from '../../../../../data/subscription-url'
|
||||||
|
|
||||||
|
export function KeepCurrentPlanModal() {
|
||||||
|
const modalId = 'keep-current-plan'
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [inflight, setInflight] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { modalIdShown, handleCloseModal, personalSubscription } =
|
||||||
|
useSubscriptionDashboardContext()
|
||||||
|
|
||||||
|
async function confirmCancelPendingPlanChange() {
|
||||||
|
setError(false)
|
||||||
|
setInflight(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postJSON(cancelPendingSubscriptionChangeUrl)
|
||||||
|
} catch (e) {
|
||||||
|
setError(true)
|
||||||
|
setInflight(false)
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalIdShown !== modalId || !personalSubscription) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleModal
|
||||||
|
id={modalId}
|
||||||
|
show
|
||||||
|
animation
|
||||||
|
onHide={handleCloseModal}
|
||||||
|
backdrop="static"
|
||||||
|
>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>{t('change_plan')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<Modal.Body>
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-warning">
|
||||||
|
{t('generic_something_went_wrong')}. {t('try_again')}.{' '}
|
||||||
|
{t('generic_if_problem_continues_contact_us')}.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="sure_you_want_to_cancel_plan_change"
|
||||||
|
values={{
|
||||||
|
planName: personalSubscription.plan.name,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
|
<strong />,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Modal.Body>
|
||||||
|
|
||||||
|
<Modal.Footer>
|
||||||
|
<button
|
||||||
|
disabled={inflight}
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={inflight}
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={confirmCancelPendingPlanChange}
|
||||||
|
>
|
||||||
|
{!inflight
|
||||||
|
? t('revert_pending_plan_change')
|
||||||
|
: t('processing_uppercase') + '…'}
|
||||||
|
</button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AccessibleModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -19,15 +19,23 @@ import { loadDisplayPriceWithTaxPromise } from '../util/recurly-pricing'
|
||||||
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
|
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
|
||||||
|
|
||||||
type SubscriptionDashboardContextValue = {
|
type SubscriptionDashboardContextValue = {
|
||||||
|
handleCloseModal: () => void
|
||||||
|
handleOpenModal: (modalIdToOpen: string, planCode?: string) => void
|
||||||
hasDisplayedSubscription: boolean
|
hasDisplayedSubscription: boolean
|
||||||
institutionMemberships?: Institution[]
|
institutionMemberships?: Institution[]
|
||||||
managedGroupSubscriptions: ManagedGroupSubscription[]
|
managedGroupSubscriptions: ManagedGroupSubscription[]
|
||||||
managedInstitutions: ManagedInstitution[]
|
managedInstitutions: ManagedInstitution[]
|
||||||
updateManagedInstitution: (institution: ManagedInstitution) => void
|
updateManagedInstitution: (institution: ManagedInstitution) => void
|
||||||
|
modalIdShown?: string
|
||||||
personalSubscription?: Subscription
|
personalSubscription?: Subscription
|
||||||
plans: Plan[]
|
plans: Plan[]
|
||||||
|
planCodeToChangeTo?: string
|
||||||
queryingIndividualPlansData: boolean
|
queryingIndividualPlansData: boolean
|
||||||
recurlyLoadError: boolean
|
recurlyLoadError: boolean
|
||||||
|
setModalIdShown: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||||
|
setPlanCodeToChangeTo: React.Dispatch<
|
||||||
|
React.SetStateAction<string | undefined>
|
||||||
|
>
|
||||||
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
|
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
showCancellation: boolean
|
showCancellation: boolean
|
||||||
setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
|
setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
@ -44,12 +52,16 @@ export function SubscriptionDashboardProvider({
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const [modalIdShown, setModalIdShown] = useState<string | undefined>()
|
||||||
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
||||||
const [showCancellation, setShowCancellation] = useState(false)
|
const [showCancellation, setShowCancellation] = useState(false)
|
||||||
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
|
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
|
||||||
const [plans, setPlans] = useState([])
|
const [plans, setPlans] = useState([])
|
||||||
const [queryingIndividualPlansData, setQueryingIndividualPlansData] =
|
const [queryingIndividualPlansData, setQueryingIndividualPlansData] =
|
||||||
useState(true)
|
useState(true)
|
||||||
|
const [planCodeToChangeTo, setPlanCodeToChangeTo] = useState<
|
||||||
|
string | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
||||||
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
||||||
|
@ -111,18 +123,36 @@ export function SubscriptionDashboardProvider({
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
const handleCloseModal = useCallback(() => {
|
||||||
|
setModalIdShown('')
|
||||||
|
setPlanCodeToChangeTo(undefined)
|
||||||
|
}, [setModalIdShown, setPlanCodeToChangeTo])
|
||||||
|
|
||||||
|
const handleOpenModal = useCallback(
|
||||||
|
(id, planCode) => {
|
||||||
|
setModalIdShown(id)
|
||||||
|
setPlanCodeToChangeTo(planCode)
|
||||||
|
},
|
||||||
|
[setModalIdShown, setPlanCodeToChangeTo]
|
||||||
|
)
|
||||||
|
|
||||||
const value = useMemo<SubscriptionDashboardContextValue>(
|
const value = useMemo<SubscriptionDashboardContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
|
handleCloseModal,
|
||||||
|
handleOpenModal,
|
||||||
hasDisplayedSubscription,
|
hasDisplayedSubscription,
|
||||||
institutionMemberships,
|
institutionMemberships,
|
||||||
managedGroupSubscriptions,
|
managedGroupSubscriptions,
|
||||||
managedInstitutions,
|
managedInstitutions,
|
||||||
updateManagedInstitution,
|
updateManagedInstitution,
|
||||||
|
modalIdShown,
|
||||||
personalSubscription,
|
personalSubscription,
|
||||||
plans,
|
plans,
|
||||||
|
planCodeToChangeTo,
|
||||||
queryingIndividualPlansData,
|
queryingIndividualPlansData,
|
||||||
recurlyLoadError,
|
recurlyLoadError,
|
||||||
|
setModalIdShown,
|
||||||
|
setPlanCodeToChangeTo,
|
||||||
setRecurlyLoadError,
|
setRecurlyLoadError,
|
||||||
showCancellation,
|
showCancellation,
|
||||||
setShowCancellation,
|
setShowCancellation,
|
||||||
|
@ -130,15 +160,21 @@ export function SubscriptionDashboardProvider({
|
||||||
setShowChangePersonalPlan,
|
setShowChangePersonalPlan,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
handleCloseModal,
|
||||||
|
handleOpenModal,
|
||||||
hasDisplayedSubscription,
|
hasDisplayedSubscription,
|
||||||
institutionMemberships,
|
institutionMemberships,
|
||||||
managedGroupSubscriptions,
|
managedGroupSubscriptions,
|
||||||
managedInstitutions,
|
managedInstitutions,
|
||||||
updateManagedInstitution,
|
updateManagedInstitution,
|
||||||
|
modalIdShown,
|
||||||
personalSubscription,
|
personalSubscription,
|
||||||
plans,
|
plans,
|
||||||
|
planCodeToChangeTo,
|
||||||
queryingIndividualPlansData,
|
queryingIndividualPlansData,
|
||||||
recurlyLoadError,
|
recurlyLoadError,
|
||||||
|
setModalIdShown,
|
||||||
|
setPlanCodeToChangeTo,
|
||||||
setRecurlyLoadError,
|
setRecurlyLoadError,
|
||||||
showCancellation,
|
showCancellation,
|
||||||
setShowCancellation,
|
setShowCancellation,
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const subscriptionUrl = '/user/subscription/update'
|
||||||
|
export const cancelPendingSubscriptionChangeUrl =
|
||||||
|
'/user/subscription/cancel-pending'
|
|
@ -1117,6 +1117,7 @@
|
||||||
"proceed_to_paypal": "Proceed to PayPal",
|
"proceed_to_paypal": "Proceed to PayPal",
|
||||||
"proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay": "Proceeding to PayPal will take you to the PayPal site to pay for your subscription.",
|
"proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay": "Proceeding to PayPal will take you to the PayPal site to pay for your subscription.",
|
||||||
"processing": "processing",
|
"processing": "processing",
|
||||||
|
"processing_uppercase": "Processing",
|
||||||
"processing_your_request": "Please wait while we process your request.",
|
"processing_your_request": "Please wait while we process your request.",
|
||||||
"professional": "Professional",
|
"professional": "Professional",
|
||||||
"project": "project",
|
"project": "project",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { fireEvent, screen } from '@testing-library/react'
|
import { fireEvent, screen } from '@testing-library/react'
|
||||||
import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
|
import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
|
||||||
import { ActiveSubscription } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
|
|
||||||
import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
|
import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
|
||||||
import {
|
import {
|
||||||
annualActiveSubscription,
|
annualActiveSubscription,
|
||||||
|
@ -11,11 +10,8 @@ import {
|
||||||
trialSubscription,
|
trialSubscription,
|
||||||
} from '../../../../fixtures/subscriptions'
|
} from '../../../../fixtures/subscriptions'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { plans } from '../../../../fixtures/plans'
|
import { cleanUpContext } from '../../../../helpers/render-with-subscription-dash-context'
|
||||||
import {
|
import { renderActiveSubscription } from '../../../../helpers/render-active-subscription'
|
||||||
cleanUpContext,
|
|
||||||
renderWithSubscriptionDashContext,
|
|
||||||
} from '../../../../helpers/render-with-subscription-dash-context'
|
|
||||||
|
|
||||||
describe('<ActiveSubscription />', function () {
|
describe('<ActiveSubscription />', function () {
|
||||||
let sendMBSpy: sinon.SinonSpy
|
let sendMBSpy: sinon.SinonSpy
|
||||||
|
@ -61,19 +57,6 @@ describe('<ActiveSubscription />', function () {
|
||||||
screen.getByRole('link', { name: 'View Your Invoices' })
|
screen.getByRole('link', { name: 'View Your Invoices' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderActiveSubscription(subscription: Subscription) {
|
|
||||||
const renderOptions = {
|
|
||||||
metaTags: [
|
|
||||||
{ name: 'ol-plans', value: plans },
|
|
||||||
{ name: 'ol-subscription', value: subscription },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
renderWithSubscriptionDashContext(
|
|
||||||
<ActiveSubscription subscription={subscription} />,
|
|
||||||
renderOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
it('renders the dash annual active subscription', function () {
|
it('renders the dash annual active subscription', function () {
|
||||||
renderActiveSubscription(annualActiveSubscription)
|
renderActiveSubscription(annualActiveSubscription)
|
||||||
expectedInActiveSubscription(annualActiveSubscription)
|
expectedInActiveSubscription(annualActiveSubscription)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { fireEvent, screen } from '@testing-library/react'
|
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||||
import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan'
|
import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan'
|
||||||
import { plans } from '../../../../../fixtures/plans'
|
import { plans } from '../../../../../fixtures/plans'
|
||||||
import {
|
import {
|
||||||
|
@ -11,40 +11,45 @@ import {
|
||||||
cleanUpContext,
|
cleanUpContext,
|
||||||
renderWithSubscriptionDashContext,
|
renderWithSubscriptionDashContext,
|
||||||
} from '../../../../../helpers/render-with-subscription-dash-context'
|
} from '../../../../../helpers/render-with-subscription-dash-context'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import fetchMock from 'fetch-mock'
|
||||||
|
import {
|
||||||
|
cancelPendingSubscriptionChangeUrl,
|
||||||
|
subscriptionUrl,
|
||||||
|
} from '../../../../../../../../../frontend/js/features/subscription/data/subscription-url'
|
||||||
|
import { renderActiveSubscription } from '../../../../../helpers/render-active-subscription'
|
||||||
|
|
||||||
describe('<ChangePlan />', function () {
|
describe('<ChangePlan />', function () {
|
||||||
|
let reloadStub: () => void
|
||||||
|
const originalLocation = window.location
|
||||||
const plansMetaTag = { name: 'ol-plans', value: plans }
|
const plansMetaTag = { name: 'ol-plans', value: plans }
|
||||||
const renderOptions = { metaTags: [plansMetaTag] }
|
|
||||||
|
beforeEach(function () {
|
||||||
|
reloadStub = sinon.stub()
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: { reload: reloadStub },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
cleanUpContext()
|
cleanUpContext()
|
||||||
|
fetchMock.reset()
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: originalLocation,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render the UI when showChangePersonalPlan is false', function () {
|
it('does not render the UI when showChangePersonalPlan is false', function () {
|
||||||
window.metaAttributesCache.delete('ol-plans')
|
window.metaAttributesCache.delete('ol-plans')
|
||||||
const { container } = renderWithSubscriptionDashContext(
|
const { container } = renderWithSubscriptionDashContext(<ChangePlan />, {
|
||||||
<ChangePlan />,
|
metaTags: [plansMetaTag],
|
||||||
renderOptions
|
})
|
||||||
)
|
|
||||||
|
|
||||||
expect(container.firstChild).to.be.null
|
expect(container.firstChild).to.be.null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the individual plans table and group plans UI', async function () {
|
it('renders the individual plans table and group plans UI', async function () {
|
||||||
renderWithSubscriptionDashContext(
|
renderActiveSubscription(annualActiveSubscription)
|
||||||
<ActiveSubscription subscription={annualActiveSubscription} />,
|
|
||||||
{
|
|
||||||
metaTags: [
|
|
||||||
{ name: 'ol-subscription', value: annualActiveSubscription },
|
|
||||||
plansMetaTag,
|
|
||||||
{
|
|
||||||
name: 'ol-recommendedCurrency',
|
|
||||||
value: 'USD',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
fireEvent.click(button)
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
@ -68,15 +73,7 @@ describe('<ChangePlan />', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', async function () {
|
it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', async function () {
|
||||||
renderWithSubscriptionDashContext(
|
renderActiveSubscription(pendingSubscriptionChange)
|
||||||
<ActiveSubscription subscription={pendingSubscriptionChange} />,
|
|
||||||
{
|
|
||||||
metaTags: [
|
|
||||||
{ name: 'ol-subscription', value: pendingSubscriptionChange },
|
|
||||||
plansMetaTag,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
fireEvent.click(button)
|
fireEvent.click(button)
|
||||||
|
@ -98,13 +95,12 @@ describe('<ChangePlan />', function () {
|
||||||
expect(container).not.to.be.null
|
expect(container).not.to.be.null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows a loading message while still querying Recurly for prices', function () {
|
it('shows a loading message while still querying Recurly for prices', async function () {
|
||||||
renderWithSubscriptionDashContext(
|
renderWithSubscriptionDashContext(
|
||||||
<ActiveSubscription subscription={pendingSubscriptionChange} />,
|
<ActiveSubscription subscription={pendingSubscriptionChange} />,
|
||||||
{
|
{
|
||||||
metaTags: [
|
metaTags: [
|
||||||
{ name: 'ol-subscription', value: pendingSubscriptionChange },
|
{ name: 'ol-subscription', value: pendingSubscriptionChange },
|
||||||
plansMetaTag,
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -112,6 +108,218 @@ describe('<ChangePlan />', function () {
|
||||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
fireEvent.click(button)
|
fireEvent.click(button)
|
||||||
|
|
||||||
screen.findByText('Loading', { exact: false })
|
await screen.findByText('Loading', { exact: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Change plan modal', function () {
|
||||||
|
it('open confirmation modal when "Change to this plan" clicked', async function () {
|
||||||
|
renderActiveSubscription(annualActiveSubscription)
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const buttons = await screen.findAllByRole('button', {
|
||||||
|
name: 'Change to this plan',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttons[0])
|
||||||
|
|
||||||
|
await screen.findByText('Are you sure you want to change plan to', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'Your existing plan and its features will remain active until the end of the current billing period.'
|
||||||
|
)
|
||||||
|
).to.be.null
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'If you wish this change to apply before the end of your current billing period, please contact us.'
|
||||||
|
)
|
||||||
|
).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows message in confirmation dialog about plan remaining active until end of term when expected', async function () {
|
||||||
|
let planIndex = 0
|
||||||
|
const planThatWillChange = plans.find((p, i) => {
|
||||||
|
if (p.planCode !== annualActiveSubscription.planCode) {
|
||||||
|
planIndex = i
|
||||||
|
}
|
||||||
|
return p.planCode !== annualActiveSubscription.planCode
|
||||||
|
})
|
||||||
|
|
||||||
|
renderActiveSubscription(annualActiveSubscription, [
|
||||||
|
{
|
||||||
|
name: 'ol-planCodesChangingAtTermEnd',
|
||||||
|
value: [planThatWillChange!.planCode],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const buttons = await screen.findAllByRole('button', {
|
||||||
|
name: 'Change to this plan',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttons[planIndex])
|
||||||
|
|
||||||
|
const confirmModal = screen.getByRole('dialog')
|
||||||
|
await within(confirmModal).findByText(
|
||||||
|
'Your existing plan and its features will remain active until the end of the current billing period.'
|
||||||
|
)
|
||||||
|
|
||||||
|
screen.getByText(
|
||||||
|
'If you wish this change to apply before the end of your current billing period, please contact us.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('changes plan after confirmed in modal', async function () {
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
|
fetchMock.post(
|
||||||
|
`${subscriptionUrl}?origin=confirmChangePlan`,
|
||||||
|
endPointResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
renderActiveSubscription(annualActiveSubscription, [
|
||||||
|
{
|
||||||
|
name: 'ol-planCodesChangingAtTermEnd',
|
||||||
|
value: [annualActiveSubscription.planCode],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const buttons = await screen.findAllByRole('button', {
|
||||||
|
name: 'Change to this plan',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttons[0])
|
||||||
|
|
||||||
|
await screen.findByText('Are you sure you want to change plan to', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
const buttonConfirm = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(buttonConfirm)
|
||||||
|
|
||||||
|
screen.getByText('processing', { exact: false })
|
||||||
|
|
||||||
|
// page is reloaded on success
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(reloadStub).to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error if changing plan failed', async function () {
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
fetchMock.post(
|
||||||
|
`${subscriptionUrl}?origin=confirmChangePlan`,
|
||||||
|
endPointResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
renderActiveSubscription(annualActiveSubscription)
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const buttons = await screen.findAllByRole('button', {
|
||||||
|
name: 'Change to this plan',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttons[0])
|
||||||
|
|
||||||
|
await screen.findByText('Are you sure you want to change plan to', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
const buttonConfirm = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(buttonConfirm)
|
||||||
|
|
||||||
|
screen.getByText('processing', { exact: false })
|
||||||
|
|
||||||
|
await screen.findByText('Sorry, something went wrong. ', { exact: false })
|
||||||
|
await screen.findByText('Please try again. ', { exact: false })
|
||||||
|
await screen.findByText('If the problem continues please contact us.', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
within(screen.getByRole('dialog'))
|
||||||
|
.getByRole('button', { name: 'Change plan' })
|
||||||
|
.getAttribute('disabled')
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Keep current plan modal', function () {
|
||||||
|
let confirmModal: HTMLElement
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
renderActiveSubscription(pendingSubscriptionChange)
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const keepPlanButton = await screen.findByRole('button', {
|
||||||
|
name: 'Keep my current plan',
|
||||||
|
})
|
||||||
|
fireEvent.click(keepPlanButton)
|
||||||
|
|
||||||
|
confirmModal = screen.getByRole('dialog')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens confirmation modal when "Keep my current plan" is clicked', async function () {
|
||||||
|
within(confirmModal).getByText(
|
||||||
|
'Are you sure you want to revert your scheduled plan change? You will remain subscribed to the',
|
||||||
|
{
|
||||||
|
exact: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
screen.getByRole('button', { name: 'Revert scheduled plan change' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps current plan when "Revert scheduled plan change" is clicked in modal', async function () {
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
|
fetchMock.post(cancelPendingSubscriptionChangeUrl, endPointResponse)
|
||||||
|
const buttonConfirm = within(confirmModal).getByRole('button', {
|
||||||
|
name: 'Revert scheduled plan change',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttonConfirm)
|
||||||
|
|
||||||
|
screen.getByText('processing', { exact: false })
|
||||||
|
|
||||||
|
// page is reloaded on success
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(reloadStub).to.have.been.called
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error if keeping plan failed', async function () {
|
||||||
|
const endPointResponse = {
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
fetchMock.post(cancelPendingSubscriptionChangeUrl, endPointResponse)
|
||||||
|
const buttonConfirm = within(confirmModal).getByRole('button', {
|
||||||
|
name: 'Revert scheduled plan change',
|
||||||
|
})
|
||||||
|
fireEvent.click(buttonConfirm)
|
||||||
|
|
||||||
|
screen.getByText('processing', { exact: false })
|
||||||
|
await screen.findByText('Sorry, something went wrong. ', { exact: false })
|
||||||
|
await screen.findByText('Please try again. ', { exact: false })
|
||||||
|
await screen.findByText('If the problem continues please contact us.', {
|
||||||
|
exact: false,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
within(screen.getByRole('dialog'))
|
||||||
|
.getByRole('button', { name: 'Revert scheduled plan change' })
|
||||||
|
.getAttribute('disabled')
|
||||||
|
).to.not.exist
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ActiveSubscription } from '../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
|
||||||
|
import { Subscription } from '../../../../../types/subscription/dashboard/subscription'
|
||||||
|
import { plans } from '../fixtures/plans'
|
||||||
|
import { renderWithSubscriptionDashContext } from './render-with-subscription-dash-context'
|
||||||
|
|
||||||
|
export function renderActiveSubscription(
|
||||||
|
subscription: Subscription,
|
||||||
|
tags: { name: string; value: string | object | Array<object> }[] = []
|
||||||
|
) {
|
||||||
|
const renderOptions = {
|
||||||
|
metaTags: [
|
||||||
|
...tags,
|
||||||
|
{ name: 'ol-plans', value: plans },
|
||||||
|
{ name: 'ol-subscription', value: subscription },
|
||||||
|
{
|
||||||
|
name: 'ol-recommendedCurrency',
|
||||||
|
value: 'USD',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
renderWithSubscriptionDashContext(
|
||||||
|
<ActiveSubscription subscription={subscription} />,
|
||||||
|
renderOptions
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue