diff --git a/packages/manager/.changeset/pr-12654-changed-1754641803662.md b/packages/manager/.changeset/pr-12654-changed-1754641803662.md new file mode 100644 index 00000000000..1980e22c8a9 --- /dev/null +++ b/packages/manager/.changeset/pr-12654-changed-1754641803662.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +IAM permissions for billing payment methods ([#12654](https://github.com/linode/manager/pull/12654)) diff --git a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.test.tsx b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.test.tsx index 01db716cfdc..758c91e2f5f 100644 --- a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.test.tsx +++ b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.test.tsx @@ -14,8 +14,26 @@ const props = { paymentMethod: undefined, }; +const queryMocks = vi.hoisted(() => ({ + userPermissions: vi.fn(() => ({ + data: { + delete_payment_method: false, + }, + })), +})); + +vi.mock('src/features/IAM/hooks/usePermissions', () => ({ + usePermissions: queryMocks.userPermissions, +})); + describe('Delete Payment Method Dialog', () => { it('renders the delete payment method dialog', () => { + queryMocks.userPermissions.mockReturnValue({ + data: { + delete_payment_method: true, + }, + }); + const screen = renderWithTheme(); const headerText = screen.getByText('Delete Payment Method'); @@ -31,6 +49,12 @@ describe('Delete Payment Method Dialog', () => { }); it('calls the corresponding functions when buttons are clicked', () => { + queryMocks.userPermissions.mockReturnValue({ + data: { + delete_payment_method: true, + }, + }); + const screen = renderWithTheme(); const deleteButton = screen.getByText('Delete'); @@ -43,4 +67,17 @@ describe('Delete Payment Method Dialog', () => { fireEvent.click(cancelButton); expect(props.onClose).toHaveBeenCalled(); }); + + it('disables the delete button if the user does not have the delete_payment_method permission', () => { + queryMocks.userPermissions.mockReturnValue({ + data: { + delete_payment_method: false, + }, + }); + + const screen = renderWithTheme(); + + const deleteButton = screen.getByText('Delete'); + expect(deleteButton).toBeDisabled(); + }); }); diff --git a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx index b513bfc088b..d818eee0f1f 100644 --- a/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx +++ b/packages/manager/src/components/PaymentMethodRow/DeletePaymentMethodDialog.tsx @@ -5,6 +5,7 @@ import { makeStyles } from 'tss-react/mui'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import CreditCard from 'src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/CreditCard'; +import { usePermissions } from 'src/features/IAM/hooks/usePermissions'; import { ThirdPartyPayment } from './ThirdPartyPayment'; @@ -35,11 +36,16 @@ export const DeletePaymentMethodDialog = React.memo((props: Props) => { const { error, loading, onClose, onDelete, open, paymentMethod } = props; const { classes } = useStyles(); + const { data: permissions } = usePermissions('account', [ + 'delete_payment_method', + ]); + const actions = ( ({ userPermissions: vi.fn(() => ({ data: { make_billing_payment: false, - update_account: false, + set_default_payment_method: false, + delete_payment_method: false, }, })), })); @@ -148,7 +149,8 @@ describe('Payment Method Row', () => { queryMocks.userPermissions.mockReturnValue({ data: { make_billing_payment: false, - update_account: true, + set_default_payment_method: false, + delete_payment_method: true, }, }); const { getByLabelText, getByText } = renderWithTheme( @@ -174,7 +176,8 @@ describe('Payment Method Row', () => { queryMocks.userPermissions.mockReturnValue({ data: { make_billing_payment: true, - update_account: true, + set_default_payment_method: true, + delete_payment_method: false, }, }); const paymentMethod = paymentMethodFactory.build({ @@ -205,7 +208,8 @@ describe('Payment Method Row', () => { queryMocks.userPermissions.mockReturnValue({ data: { make_billing_payment: false, - update_account: false, + set_default_payment_method: false, + delete_payment_method: false, }, }); const { getByLabelText, getByText } = renderWithTheme( @@ -231,7 +235,8 @@ describe('Payment Method Row', () => { queryMocks.userPermissions.mockReturnValue({ data: { make_billing_payment: true, - update_account: false, + set_default_payment_method: false, + delete_payment_method: false, }, }); const { getByLabelText, getByText } = renderWithTheme( diff --git a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx index 4a2f6fe98aa..d8c65888d25 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx @@ -45,7 +45,8 @@ export const PaymentMethodRow = (props: Props) => { const { data: permissions } = usePermissions('account', [ 'make_billing_payment', - 'update_account', + 'set_default_payment_method', + 'delete_payment_method', ]); const makeDefault = () => { @@ -74,7 +75,9 @@ export const PaymentMethodRow = (props: Props) => { }, { disabled: - isChildUser || !permissions.update_account || paymentMethod.is_default, + isChildUser || + !permissions.set_default_payment_method || + paymentMethod.is_default, onClick: makeDefault, title: 'Make Default', tooltip: paymentMethod.is_default @@ -83,7 +86,9 @@ export const PaymentMethodRow = (props: Props) => { }, { disabled: - isChildUser || !permissions.update_account || paymentMethod.is_default, + isChildUser || + !permissions.delete_payment_method || + paymentMethod.is_default, onClick: onDelete, title: 'Delete', tooltip: paymentMethod.is_default diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx index 06fa6b3d9a4..8ebe2291c8a 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx @@ -9,6 +9,7 @@ import NumberFormat from 'react-number-format'; import type { NumberFormatProps } from 'react-number-format'; import { makeStyles } from 'tss-react/mui'; +import { usePermissions } from 'src/features/IAM/hooks/usePermissions'; import { parseExpiryYear } from 'src/utilities/creditCard'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; @@ -152,9 +153,16 @@ export const AddCreditCardForm = (props: Props) => { }; const disableInput = isSubmitting || disabled; + const { data: permissions } = usePermissions('account', [ + 'create_payment_method', + ]); const disableAddButton = - disabled || !values.card_number || !values.cvv || !values.expiry_month; + disabled || + !values.card_number || + !values.cvv || + !values.expiry_month || + !permissions?.create_payment_method; return (
diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx index 5f468416397..5be9c45dac9 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddPaymentMethodDrawer.tsx @@ -13,7 +13,7 @@ import * as React from 'react'; import { LinearProgress } from 'src/components/LinearProgress'; import { MAXIMUM_PAYMENT_METHODS } from 'src/constants'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; +import { usePermissions } from 'src/features/IAM/hooks/usePermissions'; import { GooglePayChip } from '../GooglePayChip'; import { PayPalChip } from '../PayPalChip'; @@ -53,11 +53,12 @@ export const AddPaymentMethodDrawer = (props: Props) => { PaymentMessage | undefined >(undefined); const isChildUser = profile?.user_type === 'child'; - const isReadOnly = - useRestrictedGlobalGrantCheck({ - globalGrantType: 'account_access', - permittedGrantLevel: 'read_write', - }) || isChildUser; + + const { data: permissions } = usePermissions('account', [ + 'create_payment_method', + ]); + + const isReadOnly = !permissions?.create_payment_method || isChildUser; React.useEffect(() => { if (open) { diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx index cfe11db70c6..5b200eae0d7 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx @@ -22,7 +22,7 @@ vi.mock('@linode/api-v4/lib/account', async () => { const queryMocks = vi.hoisted(() => ({ useProfile: vi.fn().mockReturnValue({}), userPermissions: vi.fn(() => ({ - data: { update_account: false, make_billing_payment: false }, + data: { create_payment_method: false }, })), })); @@ -98,7 +98,7 @@ describe('Payment Info Panel', () => { it('Opens "Add Payment Method" drawer when "Add Payment Method" is clicked', async () => { queryMocks.userPermissions.mockReturnValue({ - data: { update_account: true, make_billing_payment: true }, + data: { create_payment_method: true }, }); const { getByTestId, findByTestId } = renderWithTheme( @@ -187,7 +187,7 @@ describe('Payment Info Panel', () => { }); queryMocks.userPermissions.mockReturnValue({ - data: { update_account: false, make_billing_payment: false }, + data: { create_payment_method: false }, }); const { getByTestId } = renderWithTheme( diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx index 09dc656af8e..4bae94c99be 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx @@ -48,9 +48,11 @@ const PaymentInformation = (props: Props) => { const isChildUser = profile?.user_type === 'child'; - const { data: permissions } = usePermissions('account', ['update_account']); + const { data: permissions } = usePermissions('account', [ + 'create_payment_method', + ]); - const isReadOnly = !permissions.update_account || isChildUser; + const isReadOnly = !permissions?.create_payment_method || isChildUser; const doDelete = () => { setDeleteLoading(true);