move self hosted purchase modals into one folder for shared resources, update banners to redirect to the self hosted purchase modal (in the case of air gapped, link to CWS. In the future, this will be a modal).

Этот коммит содержится в:
Conor Macpherson
2023-04-03 17:02:54 -04:00
родитель 09e93fc5e4
Коммит 0ce1b6c12c
32 изменённых файлов: 214 добавлений и 40 удалений

Просмотреть файл

@@ -23,6 +23,8 @@ import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
import useCanSelfHostedExpand from 'components/common/hooks/useCanSelfHostedExpand';
import {getExpandSeatsLink} from 'selectors/cloud';
import useControlSelfHostedExpansionModal from 'components/common/hooks/useControlSelfHostedExpansionModal';
import {useQuery} from 'utils/http_utils';
import {STORAGE_KEY_EXPANSION_IN_PROGRESS} from 'components/self_hosted_purchases/constants';
const DAYS_UNTIL_EXPIRY_WARNING_DISPLAY_THRESHOLD = 30;
const DAYS_UNTIL_EXPIRY_DANGER_DISPLAY_THRESHOLD = 5;
@@ -58,6 +60,19 @@ const EnterpriseEditionLeftPanel = ({
const canExpand = useCanSelfHostedExpand();
const selfHostedExpansionModal = useControlSelfHostedExpansionModal({trackingLocation: 'license_settings_add_seats'});
const expandableLink = useSelector(getExpandSeatsLink);
const isSelfHostedExpansionEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedExpansion;
const query = useQuery();
const actionQueryParam = query.get('action');
useEffect(() => {
console.log(actionQueryParam);
if (actionQueryParam === 'show_expansion_modal' && canExpand && isSelfHostedExpansionEnabled) {
console.log("Open modal!");
selfHostedExpansionModal.open();
query.set('action', '');
}
}, [])
useEffect(() => {
async function fetchUnSanitizedLicense() {
@@ -73,7 +88,6 @@ const EnterpriseEditionLeftPanel = ({
const skuName = getSkuDisplayName(unsanitizedLicense.SkuShortName, unsanitizedLicense.IsGovSku === 'true');
const expirationDays = getRemainingDaysFromFutureTimestamp(parseInt(unsanitizedLicense.ExpiresAt, 10));
const isSelfHostedExpansionEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedExpansion;
const viewPlansButton = (
<button

Просмотреть файл

@@ -4,6 +4,7 @@
import React, {useMemo} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {useHistory} from 'react-router-dom';
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {GlobalState} from 'types/store';
@@ -16,9 +17,10 @@ import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {PreferenceType} from '@mattermost/types/preferences';
import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants';
import {StatTypes, Preferences, AnnouncementBarTypes, ConsolePages} from 'utils/constants';
import './overage_users_banner.scss';
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
type AdminHasDismissedItArgs = {
preferenceName: string;
@@ -56,6 +58,8 @@ const OverageUsersBanner = () => {
const prefixPreferences = isOver10PercerntPurchasedSeats ? 'error' : 'warn';
const prefixLicenseId = (license.Id || '').substring(0, 8);
const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`;
const history = useHistory();
const isAirGapped = !useCWSAvailabilityCheck();
const overageByUsers = activeUsers - seatsPurchased;
@@ -86,7 +90,14 @@ const OverageUsersBanner = () => {
const handleUpdateSeatsSelfServeClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
trackEventFn('Self Serve');
window.open(expandableLink(license.Id), '_blank');
if (isAirGapped) {
window.open(expandableLink(license.Id), '_blank');
}
if (isExpandable) {
history.push(`${ConsolePages.LICENSE}?action=show_expansion_modal`);
}
};
const handleContactSalesClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {

Просмотреть файл

@@ -12,8 +12,8 @@ import {Client4} from 'mattermost-redux/client';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
import {HostedCustomerTypes} from 'mattermost-redux/action_types';
import {STORAGE_KEY_EXPANSION_IN_PROGRESS} from 'components/self_hosted_expansion_modal/constants';
import SelfHostedExpansionModal from 'components/self_hosted_expansion_modal';
import {STORAGE_KEY_EXPANSION_IN_PROGRESS} from 'components/self_hosted_purchases/constants';
import SelfHostedExpansionModal from 'components/self_hosted_purchases/self_hosted_expansion_modal';
import {useControlModal, ControlModal} from './useControlModal';
import useCanSelfHostedExpand from './useCanSelfHostedExpand';

Просмотреть файл

@@ -7,8 +7,8 @@ import {useDispatch, useSelector} from 'react-redux';
import {trackEvent} from 'actions/telemetry_actions';
import {closeModal, openModal} from 'actions/views/modals';
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
import SelfHostedPurchaseModal from 'components/self_hosted_purchase_modal';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from 'components/self_hosted_purchase_modal/constants';
import SelfHostedPurchaseModal from 'components/self_hosted_purchases/self_hosted_purchase_modal';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from 'components/self_hosted_purchases/constants';
import PurchaseInProgressModal from 'components/purchase_in_progress_modal';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';

Просмотреть файл

@@ -16,10 +16,13 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {PreferenceType} from '@mattermost/types/preferences';
import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck';
import {LicenseLinks, StatTypes, Preferences} from 'utils/constants';
import {LicenseLinks, StatTypes, Preferences, ConsolePages} from 'utils/constants';
import './overage_users_banner_notice.scss';
import ExternalLink from 'components/external_link';
import useControlSelfHostedExpansionModal from 'components/common/hooks/useControlSelfHostedExpansionModal';
import {NavLink} from 'react-router-dom';
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
type AdminHasDismissedArgs = {
preferenceName: string;
@@ -53,15 +56,16 @@ const OverageUsersBannerNotice = () => {
const prefixLicenseId = (license.Id || '').substring(0, 8);
const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`;
const isAirGapped = !useCWSAvailabilityCheck();
const overageByUsers = activeUsers - seatsPurchased;
const isOverageState = isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats;
const hasPermission = isAdmin && isOverageState && !isCloud;
const {
cta,
expandableLink,
trackEventFn,
getRequestState,
isExpandable,
expandableLink,
} = useExpandOverageUsersCheck({
shouldRequest: hasPermission && !adminHasDismissed({overagePreferences, preferenceName}),
licenseId: license.Id,
@@ -69,6 +73,8 @@ const OverageUsersBannerNotice = () => {
banner: 'invite modal',
});
const selfHostedExpansionModal = useControlSelfHostedExpansionModal({trackingLocation: 'overage_user_banner_notice'});
if (!hasPermission || adminHasDismissed({overagePreferences, preferenceName})) {
return null;
}
@@ -96,12 +102,31 @@ const OverageUsersBannerNotice = () => {
const handleClick = () => {
trackEventFn(isExpandable ? 'Self Serve' : 'Contact Sales');
if (isExpandable) {
selfHostedExpansionModal.open()
}
};
if (isAirGapped) {
window.open(expandableLink(license.Id), '_blank');
}
if (isExpandable) {
return (
<NavLink
to={`${ConsolePages.LICENSE}?action=show_expansion_modal`}
target='_blank'
rel='noreferrer'
>
{cta}
</NavLink>
)
}
return (
<ExternalLink
className='overage_users_banner__button'
href={isExpandable ? expandableLink(license.Id) : LicenseLinks.CONTACT_SALES}
href={LicenseLinks.CONTACT_SALES}
onClick={handleClick}
>
{cta}

Просмотреть файл

@@ -11,7 +11,7 @@ import {GlobalState} from 'types/store';
import {TestHelper as TH} from 'utils/test_helper';
import {Client4} from 'mattermost-redux/client';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from 'components/self_hosted_purchase_modal/constants';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from 'components/self_hosted_purchases/constants';
import PurchaseInProgressModal from './';

Просмотреть файл

@@ -1,4 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const STORAGE_KEY_EXPANSION_IN_PROGRESS = 'EXPANSION_IN_PROGRESS';

Просмотреть файл

@@ -2,3 +2,4 @@
// See LICENSE.txt for license information.
export const STORAGE_KEY_PURCHASE_IN_PROGRESS = 'PURCHASE_IN_PROGRESS';
export const STORAGE_KEY_EXPANSION_IN_PROGRESS = 'EXPANSION_IN_PROGRESS';

Просмотреть файл

@@ -13,6 +13,7 @@ import './error_page.scss';
interface Props {
canRetry: boolean;
tryAgain: () => void;
}
export default function SelfHostedExpansionErrorPage(props: Props) {
@@ -70,9 +71,7 @@ export default function SelfHostedExpansionErrorPage(props: Props) {
icon={icon}
error={true}
formattedButtonText={formattedButtonText}
buttonHandler={() => {
//TODO: Open self hosted expansion modal
}}
buttonHandler={props.tryAgain}
formattedTertiaryButonText={tertiaryButtonText}
tertiaryButtonHandler={() => window.open(contactSupportLink, '_blank', 'noreferrer')}
/>

Просмотреть файл

@@ -11,10 +11,10 @@ import {StripeCardElementChangeEvent} from '@stripe/stripe-js';
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
import RootPortal from 'components/root_portal';
import ContactSalesLink from 'components/self_hosted_purchase_modal/contact_sales_link';
import ErrorPage from 'components/self_hosted_expansion_modal/error_page';
import SuccessPage from 'components/self_hosted_expansion_modal/success_page';
import Submitting from 'components/self_hosted_purchase_modal/submitting';
import ContactSalesLink from 'components/self_hosted_purchases/contact_sales_link';
import ErrorPage from 'components/self_hosted_purchases/self_hosted_expansion_modal/error_page';
import SuccessPage from 'components/self_hosted_purchases/self_hosted_expansion_modal/success_page';
import Submitting from './submitting';
import useLoadStripe from 'components/common/hooks/useLoadStripe';
import CardInput, {CardInputType} from 'components/payment_form/card_input';
@@ -23,7 +23,7 @@ import Input from 'components/widgets/inputs/input/input';
import BackgroundSvg from 'components/common/svg_images_components/background_svg';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import StripeProvider from '../self_hosted_purchase_modal/stripe_provider';
import StripeProvider from '../stripe_provider';
import {closeModal} from 'actions/views/modals';
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
@@ -46,11 +46,12 @@ import SelfHostedExpansionCard from './expansion_card';
import './self_hosted_expansion_modal.scss';
import {STORAGE_KEY_EXPANSION_IN_PROGRESS} from './constants';
import Address from 'components/self_hosted_purchase_modal/address';
import {STORAGE_KEY_EXPANSION_IN_PROGRESS} from '../constants';
import Address from 'components/self_hosted_purchases/address';
import ChooseDifferentShipping from 'components/choose_different_shipping';
import Terms from 'components/self_hosted_purchase_modal/terms';
import Terms from 'components/self_hosted_purchases/self_hosted_purchase_modal/terms';
import useControlSelfHostedExpansionModal from 'components/common/hooks/useControlSelfHostedExpansionModal';
import classNames from 'classnames';
export interface FormState {
cardName: string;
@@ -189,6 +190,7 @@ export default function SelfHostedExpansionModal() {
const [formState, setFormState] = useState<FormState>(initialState);
const [show] = useState(true);
const canRetry = formState.error !== '422';
const showForm = progress !== SelfHostedSignupProgress.PAID && progress !== SelfHostedSignupProgress.CREATED_LICENSE && !formState.submitting && !formState.error && !formState.succeeded;
const title = intl.formatMessage({
id: 'self_hosted_expansion.expansion_modal.title',
@@ -200,6 +202,7 @@ export default function SelfHostedExpansionModal() {
const submit = async () => {
let submitProgress = progress;
let signupCustomerResult: SelfHostedSignupCustomerResponse | null = null;
setFormState({...formState, submitting: true});
try {
const [firstName, lastName] = inferNames(user, formState.cardName);
@@ -344,8 +347,7 @@ export default function SelfHostedExpansionModal() {
}}
>
<div className='SelfHostedExpansionModal'>
<div className='form-view'>
<div className='lhs'>
{<div className={classNames('form-view', {'form-view--hide': !showForm})}> <div className='lhs'>
<h2 className='title'>{title}</h2>
<UpgradeSvg
width={267}
@@ -503,21 +505,21 @@ export default function SelfHostedExpansionModal() {
initialSeats={additionalSeats}
/>
</div>
</div>
</div>}
{((formState.succeeded || progress === SelfHostedSignupProgress.CREATED_LICENSE)) && !formState.error && !formState.submitting && (
<SuccessPage
onClose={selfHostedExpansionModal.close}
/>
)}
{formState.submitting && !formState.error && (
{formState.submitting && (
<Submitting
desiredPlanName={currentPlan}
progressBar={formState.progressBar}
currentPlan={currentPlan}
/>
)}
{formState.error && (
<ErrorPage
canRetry={canRetry}
tryAgain={() => {setFormState({...formState, submitting: false, error: ''})}}
/>
)}
<div className='background-svg'>

Просмотреть файл

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {getSelfHostedSignupProgress} from 'mattermost-redux/selectors/entities/hosted_customer';
import {SelfHostedSignupProgress} from '@mattermost/types/hosted_customer';
import {ValueOf} from '@mattermost/types/utilities';
import CreditCardSvg from 'components/common/svg_images_components/credit_card_svg';
import IconMessage from 'components/purchase_modal/icon_message';
function useConvertProgressToWaitingExplanation(progress: ValueOf<typeof SelfHostedSignupProgress>, planName: string): React.ReactNode {
const intl = useIntl();
switch (progress) {
case SelfHostedSignupProgress.START:
case SelfHostedSignupProgress.CREATED_CUSTOMER:
case SelfHostedSignupProgress.CREATED_INTENT:
return intl.formatMessage({
id: 'self_hosted_signup.progress_step.submitting_payment',
defaultMessage: 'Submitting payment information',
});
case SelfHostedSignupProgress.CONFIRMED_INTENT:
case SelfHostedSignupProgress.CREATED_SUBSCRIPTION:
return intl.formatMessage({
id: 'self_hosted_signup.progress_step.verifying_payment',
defaultMessage: 'Verifying payment details',
});
case SelfHostedSignupProgress.PAID:
case SelfHostedSignupProgress.CREATED_LICENSE:
return intl.formatMessage({
id: 'self_hosted_signup.progress_step.applying_license',
defaultMessage: 'Applying your {planName} license to your Mattermost instance',
}, {planName});
default:
return intl.formatMessage({
id: 'self_hosted_signup.progress_step.submitting_payment',
defaultMessage: 'Submitting payment information',
});
}
}
export function convertProgressToBar(progress: ValueOf<typeof SelfHostedSignupProgress>): number {
switch (progress) {
case SelfHostedSignupProgress.START:
return 15;
case SelfHostedSignupProgress.CREATED_CUSTOMER:
return 30;
case SelfHostedSignupProgress.CREATED_INTENT:
return 45;
case SelfHostedSignupProgress.CONFIRMED_INTENT:
return 60;
case SelfHostedSignupProgress.CREATED_SUBSCRIPTION:
return 75;
case SelfHostedSignupProgress.PAID:
return 85;
case SelfHostedSignupProgress.CREATED_LICENSE:
return 100;
default:
return 0;
}
}
interface Props {
currentPlan: string;
}
const maxProgressBar = 100;
const maxFakeProgressIncrement = 5;
const fakeProgressInterval = 600;
export default function Submitting(props: Props) {
const [barProgress, setBarProgress] = useState(0);
const signupProgress = useSelector(getSelfHostedSignupProgress);
const waitingExplanation = useConvertProgressToWaitingExplanation(signupProgress, props.currentPlan);
const footer = (
<div className='ProcessPayment-progress'>
<div
className='ProcessPayment-progress-fill'
style={{width: `${barProgress}%`}}
/>
</div>
);
useEffect(() => {
const maxProgressForCurrentSignupProgress = convertProgressToBar(signupProgress);
const interval = setInterval(() => {
if (barProgress < maxProgressBar) {
setBarProgress(Math.min(maxProgressForCurrentSignupProgress, barProgress + maxFakeProgressIncrement));
}
}, fakeProgressInterval);
return () => clearInterval(interval);
}, [barProgress]);
return (
<div className='submitting'>
<IconMessage
formattedTitle={(
<FormattedMessage
id='admin.billing.subscription.verifyPaymentInformation'
defaultMessage='Verifying your payment information'
/>
)}
formattedSubtitle={waitingExplanation}
icon={
<CreditCardSvg
width={444}
height={313}
/>
}
footer={footer}
className={'processing'}
/>
</div>
);
}

Просмотреть файл

@@ -15,7 +15,7 @@ import {SelfHostedProducts, ModalIdentifiers} from 'utils/constants';
import {DeepPartial} from '@mattermost/types/utilities';
import SelfHostedPurchaseModal, {makeInitialState, canSubmit, State} from './';
import SelfHostedPurchaseModal, {makeInitialState, canSubmit, State} from '.';
interface MockCardInputProps {
onCardInputChange: (event: {complete: boolean}) => void;

Просмотреть файл

@@ -53,23 +53,23 @@ import {
SelfHostedSignupCustomerResponse,
} from '@mattermost/types/hosted_customer';
import {Seats, errorInvalidNumber} from '../seats_calculator';
import {Seats, errorInvalidNumber} from '../../seats_calculator';
import ContactSalesLink from './contact_sales_link';
import ContactSalesLink from '../contact_sales_link';
import Submitting, {convertProgressToBar} from './submitting';
import ErrorPage from './error';
import SuccessPage from './success_page';
import SelfHostedCard from './self_hosted_card';
import StripeProvider from './stripe_provider';
import StripeProvider from '../stripe_provider';
import Terms from './terms';
import Address from './address';
import Address from '../address';
import useNoEscape from './useNoEscape';
import {SetPrefix, UnionSetActions} from './types';
import './self_hosted_purchase_modal.scss';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from './constants';
import {STORAGE_KEY_PURCHASE_IN_PROGRESS} from '../constants';
export interface State {

Просмотреть файл

@@ -17,8 +17,8 @@ import {
SelfHostedProducts,
} from 'utils/constants';
import Consequences from '../seats_calculator/consequences';
import SeatsCalculator, {Seats} from '../seats_calculator';
import Consequences from '../../seats_calculator/consequences';
import SeatsCalculator, {Seats} from '../../seats_calculator';
// Card has a bunch of props needed for monthly/yearly payments that
// do not apply to self-hosted.