Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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<HTMLFormElement>) => {
e.preventDefault();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -176,7 +191,11 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => {
<Form.SubmitButton
block={false}
isDisabled={!canSubmit}
localizationKey={localizationKeys('organizationProfile.invitePage.formButtonPrimary__continue')}
localizationKey={
isPerSeatCostPlan && mustPurchaseSeats
? localizationKeys('organizationProfile.invitePage.formButtonPrimary__purchaseSeats')
: localizationKeys('organizationProfile.invitePage.formButtonPrimary__continue')
}
/>
<Form.ResetButton
localizationKey={resetButtonLabel || localizationKeys('userProfile.formButtonReset')}
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/utils/billingPlanSeats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type {
BillingPerUnitTotalTier,
BillingPlanResource,
BillingPlanUnitPrice,
BillingPlanUnitPriceTier,
BillingSubscriptionItemResource,
OrganizationResource,
} from '@clerk/shared/types';

Expand Down Expand Up @@ -86,6 +88,26 @@ export const getIncludedSeatsUnitTotalTier = (
return null;
};

export const getPaidSeatsUnitTier = (unitPrice: BillingPlanUnitPrice | null): BillingPlanUnitPriceTier | null => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to add a unit test for this helper

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.
*/
Expand Down Expand Up @@ -114,3 +136,18 @@ export const organizationExceedsPlanSeatLimit = (

return organization.membersCount + organization.pendingInvitationsCount > seatLimit;
};

export const organizationAndInvitationsExceedsPurchasedSeats = (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is pretty simple, but perhaps a happy path unit test?

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
);
};
Loading