From f5868fde2bb53fa73c13bccd9f868610394af784 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 7 Aug 2025 09:39:57 +0200 Subject: [PATCH 1/6] AddPaymentMethodDrawer --- .../AddPaymentMethodDrawer.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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) { From d9e6f5889eb1e9887891ce577c2b456881d3895c Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 7 Aug 2025 22:51:30 +0200 Subject: [PATCH 2/6] PaymentInformation --- .../AddPaymentMethodDrawer/AddCreditCardForm.tsx | 10 +++++++++- .../PaymentInfoPanel/PaymentInformation.tsx | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) 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/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); From 483126859af64856f7d7de6f159e1ad38230040a Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 7 Aug 2025 23:04:05 +0200 Subject: [PATCH 3/6] PaymentMethodRow --- .../components/PaymentMethodRow/PaymentMethodRow.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 From 0204af376ff5c796733f3e845cd476de99f33bab Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 7 Aug 2025 23:07:54 +0200 Subject: [PATCH 4/6] remaining checks --- .../PaymentMethodRow/DeletePaymentMethodDialog.tsx | 6 ++++++ 1 file changed, 6 insertions(+) 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 = ( Date: Fri, 8 Aug 2025 10:04:24 +0200 Subject: [PATCH 5/6] fix tests --- .../DeletePaymentMethodDialog.test.tsx | 37 +++++++++++++++++++ .../PaymentMethodRow.test.tsx | 15 +++++--- .../PaymentInformation.test.tsx | 6 +-- 3 files changed, 50 insertions(+), 8 deletions(-) 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/PaymentMethodRow.test.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx index 6fc3c569ed4..62810bba634 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx @@ -15,7 +15,8 @@ const queryMocks = vi.hoisted(() => ({ 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/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( From 933dbb7103e339b6ffcbcf4091d7689bf730d8df Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Fri, 8 Aug 2025 10:30:03 +0200 Subject: [PATCH 6/6] Added changeset: IAM permissions for billing payment methods --- .../manager/.changeset/pr-12654-changed-1754641803662.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-12654-changed-1754641803662.md 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))