diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index be7dcff8482..7d91a056ccc 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -535,6 +535,7 @@ export const enUS: LocalizationResource = { detailsTitle__inviteFailed: 'The invitations could not be sent. There are already pending invitations for the following email addresses: {{email_addresses}}.', formButtonPrimary__continue: 'Send invitations', + formButtonPrimary__purchaseSeats: 'Purchase additional seats', selectDropdown__role: 'Select role', subtitle: 'Enter or paste one or more email addresses, separated by spaces or commas.', successMessage: 'Invitations successfully sent', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 3bb00ce6831..c53b1ecd020 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1110,6 +1110,7 @@ export type __internal_LocalizationResource = { successMessage: LocalizationValue; detailsTitle__inviteFailed: LocalizationValue<'email_addresses'>; formButtonPrimary__continue: LocalizationValue; + formButtonPrimary__purchaseSeats: LocalizationValue; selectDropdown__role: LocalizationValue; }; removeDomainPage: { diff --git a/packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx b/packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx index 3a8ecc68b8d..38767d1691b 100644 --- a/packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx @@ -8,11 +8,16 @@ import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; import { TagInput } from '@/ui/elements/TagInput'; +import { + getPaidSeatsUnitTier, + getSeatUnitPrice, + organizationAndInvitationsExceedsPurchasedSeats, +} from '@/ui/utils/billingPlanSeats'; import { handleError } from '@/ui/utils/errorHandler'; import { createListFormat } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; -import { useEnvironment } from '../../contexts'; +import { useEnvironment, useSubscription } from '../../contexts'; import { Flex } from '../../customizables'; import { useFetchRoles } from '../../hooks/useFetchRoles'; import type { LocalizationKey } from '../../localization'; @@ -37,6 +42,8 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { keepPreviousData: true, }, }); + const { data: subscription } = useSubscription(); + const activeSubscriptionItem = subscription?.subscriptionItems.find(si => si.status === 'active'); const card = useCardState(); const { t, locale } = useLocalizations(); const [isValidUnsubmittedEmail, setIsValidUnsubmittedEmail] = useState(false); @@ -74,6 +81,14 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { } = emailAddressField; const canSubmit = (!!emailAddressField.value.length || isValidUnsubmittedEmail) && !!roleField.value; + const emailAddresses = emailAddressField.value.split(','); + + const seatUnitPrice = activeSubscriptionItem ? getSeatUnitPrice(activeSubscriptionItem.plan) : null; + const paidSeatsTier = seatUnitPrice ? getPaidSeatsUnitTier(seatUnitPrice) : null; + const isPerSeatCostPlan = !!paidSeatsTier; + const mustPurchaseSeats = + isPerSeatCostPlan && + organizationAndInvitationsExceedsPurchasedSeats(activeSubscriptionItem, organization, emailAddresses.length); const onSubmit = (e: FormEvent) => { e.preventDefault(); @@ -84,7 +99,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { const submittedData = new FormData(e.currentTarget); return organization .inviteMembers({ - emailAddresses: emailAddressField.value.split(','), + emailAddresses, role: submittedData.get('role') as string, }) .then(async () => { @@ -176,7 +191,11 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { { + if (!unitPrice) { + return null; + } + + if (unitPrice.tiers.length === 1 && unitPrice.tiers[0].feePerBlock.amount > 0) { + return unitPrice.tiers[0]; + } + + if ( + unitPrice.tiers.length === 2 && + unitPrice.tiers[0].feePerBlock.amount === 0 && + unitPrice.tiers[1].feePerBlock.amount > 0 + ) { + return unitPrice.tiers[1]; + } + + return null; +}; + /** * Given a plan, return the seat limit for the plan, or undefined if the plan does not have a seat limit. */ @@ -114,3 +136,18 @@ export const organizationExceedsPlanSeatLimit = ( return organization.membersCount + organization.pendingInvitationsCount > seatLimit; }; + +export const organizationAndInvitationsExceedsPurchasedSeats = ( + subscriptionItem: BillingSubscriptionItemResource | undefined, + organization: OrganizationResource, + invitationsCount: number, +): boolean => { + if (!subscriptionItem || !subscriptionItem.seats || !subscriptionItem.seats.quantity) { + return false; + } + + return ( + organization.membersCount + organization.pendingInvitationsCount + invitationsCount > + subscriptionItem.seats.quantity + ); +};