diff --git a/packages/api-v4/.changeset/pr-10080-upcoming-features-1705618109652.md b/packages/api-v4/.changeset/pr-10080-upcoming-features-1705618109652.md new file mode 100644 index 00000000000..6fc3fdf87d1 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10080-upcoming-features-1705618109652.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Add `user_type` to /profile endpoint for Parent/Child user roles ([#10080](https://github.com/linode/manager/pull/10080)) diff --git a/packages/api-v4/src/profile/types.ts b/packages/api-v4/src/profile/types.ts index 49d16124f26..0d7ea3347f3 100644 --- a/packages/api-v4/src/profile/types.ts +++ b/packages/api-v4/src/profile/types.ts @@ -1,3 +1,5 @@ +import type { UserType } from '../account'; + export interface Referrals { code: string; url: string; @@ -22,6 +24,7 @@ export interface Profile { two_factor_auth: boolean; restricted: boolean; verified_phone_number: string | null; + user_type: UserType | null; } export interface TokenRequest { diff --git a/packages/manager/.changeset/pr-10080-upcoming-features-1705618128252.md b/packages/manager/.changeset/pr-10080-upcoming-features-1705618128252.md new file mode 100644 index 00000000000..612dc91a274 --- /dev/null +++ b/packages/manager/.changeset/pr-10080-upcoming-features-1705618128252.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add `user_type` to /profile endpoint for Parent/Child user roles ([#10080](https://github.com/linode/manager/pull/10080)) diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index 370be9b3c05..5a17dd052d0 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -277,11 +277,11 @@ describe('restricted user billing flows', () => { it('cannot edit billing information as child account', () => { const mockProfile = profileFactory.build({ username: randomLabel(), + user_type: 'child', }); const mockUser = accountUserFactory.build({ username: mockProfile.username, - user_type: 'child', }); mockGetProfile(mockProfile); diff --git a/packages/manager/src/factories/profile.ts b/packages/manager/src/factories/profile.ts index bb16f9a44bf..86fd7603653 100644 --- a/packages/manager/src/factories/profile.ts +++ b/packages/manager/src/factories/profile.ts @@ -25,6 +25,7 @@ export const profileFactory = Factory.Sync.makeFactory({ timezone: 'Asia/Shanghai', two_factor_auth: false, uid: 9999, + user_type: null, username: 'mock-user', verified_phone_number: '+15555555555', }); diff --git a/packages/manager/src/features/Billing/BillingDetail.tsx b/packages/manager/src/features/Billing/BillingDetail.tsx index db6fbcb6489..07b4ce95396 100644 --- a/packages/manager/src/features/Billing/BillingDetail.tsx +++ b/packages/manager/src/features/Billing/BillingDetail.tsx @@ -11,6 +11,7 @@ import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { PAYPAL_CLIENT_ID } from 'src/constants'; import { useAccount } from 'src/queries/account'; import { useAllPaymentMethodsQuery } from 'src/queries/accountPayment'; +import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import BillingActivityPanel from './BillingPanels/BillingActivityPanel/BillingActivityPanel'; @@ -31,6 +32,8 @@ export const BillingDetail = () => { isLoading: accountLoading, } = useAccount(); + const { data: profile } = useProfile(); + if (accountLoading) { return ; } @@ -75,6 +78,7 @@ export const BillingDetail = () => { firstName={account.first_name} lastName={account.last_name} phone={account.phone} + profile={profile} state={account.state} taxId={account.tax_id} zip={account.zip} @@ -84,6 +88,7 @@ export const BillingDetail = () => { isAkamaiCustomer={account?.billing_source === 'akamai'} loading={paymentMethodsLoading} paymentMethods={paymentMethods} + profile={profile} /> diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.test.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.test.tsx index 653217ba7a1..836d413eef6 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.test.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.test.tsx @@ -1,13 +1,18 @@ import * as React from 'react'; +import { profileFactory } from 'src/factories'; import { grantsFactory } from 'src/factories/grants'; -import { accountUserFactory } from 'src/factories/accountUsers'; import { renderWithTheme } from 'src/utilities/testHelpers'; import ContactInformation from './ContactInformation'; const EDIT_BUTTON_ID = 'edit-contact-info'; +const queryMocks = vi.hoisted(() => ({ + useGrants: vi.fn().mockReturnValue({}), + useProfile: vi.fn().mockReturnValue({}), +})); + const props = { address1: '123 Linode Lane', address2: '', @@ -18,21 +23,17 @@ const props = { firstName: 'Linny', lastName: 'The Platypus', phone: '19005553221', + profile: queryMocks.useProfile().data, state: 'PA', taxId: '1337', zip: '19106', }; -const queryMocks = vi.hoisted(() => ({ - useAccountUser: vi.fn().mockReturnValue({}), - useGrants: vi.fn().mockReturnValue({}), -})); - -vi.mock('src/queries/accountUsers', async () => { - const actual = await vi.importActual('src/queries/accountUsers'); +vi.mock('src/queries/profile', async () => { + const actual = await vi.importActual('src/queries/profile'); return { ...actual, - useAccountUser: queryMocks.useAccountUser, + useProfile: queryMocks.useProfile, }; }); @@ -44,19 +45,20 @@ vi.mock('src/queries/profile', async () => { }; }); -queryMocks.useAccountUser.mockReturnValue({ - data: accountUserFactory.build({ user_type: 'parent' }), -}); - describe('Edit Contact Information', () => { it('should be disabled for all child users', () => { - queryMocks.useAccountUser.mockReturnValue({ - data: accountUserFactory.build({ user_type: 'child' }), + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ + user_type: 'child', + }), }); - const { getByTestId } = renderWithTheme(, { - flags: { parentChildAccountAccess: true }, - }); + const { getByTestId } = renderWithTheme( + , + { + flags: { parentChildAccountAccess: true }, + } + ); expect(getByTestId(EDIT_BUTTON_ID)).toHaveAttribute( 'aria-disabled', diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx index cff393293be..f762ae76668 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx @@ -8,8 +8,7 @@ import { Typography } from 'src/components/Typography'; import { getDisabledTooltipText } from 'src/features/Billing/billingUtils'; import { EDIT_BILLING_CONTACT } from 'src/features/Billing/constants'; import { useFlags } from 'src/hooks/useFlags'; -import { useAccountUser } from 'src/queries/accountUsers'; -import { useGrants, useProfile } from 'src/queries/profile'; +import { useGrants } from 'src/queries/profile'; import { BillingActionButton, @@ -18,6 +17,8 @@ import { } from '../../BillingDetail'; import BillingContactDrawer from './EditBillingContactDrawer'; +import type { Profile } from '@linode/api-v4'; + interface Props { address1: string; address2: string; @@ -28,6 +29,7 @@ interface Props { firstName: string; lastName: string; phone: string; + profile: Profile | undefined; state: string; taxId: string; zip: string; @@ -57,6 +59,7 @@ const ContactInformation = (props: Props) => { firstName, lastName, phone, + profile, state, taxId, zip, @@ -75,11 +78,10 @@ const ContactInformation = (props: Props) => { const [focusEmail, setFocusEmail] = React.useState(false); const flags = useFlags(); - const { data: profile } = useProfile(); const { data: grants } = useGrants(); - const { data: user } = useAccountUser(profile?.username ?? ''); + const isChildUser = - flags.parentChildAccountAccess && user?.user_type === 'child'; + flags.parentChildAccountAccess && profile?.user_type === 'child'; const isRestrictedUser = isChildUser || grants?.global.account_access === 'read_only'; 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 0bfb0ff1954..19b70492b68 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.test.tsx @@ -3,9 +3,8 @@ import { fireEvent } from '@testing-library/react'; import * as React from 'react'; import { PAYPAL_CLIENT_ID } from 'src/constants'; -import { paymentMethodFactory } from 'src/factories'; import { profileFactory } from 'src/factories'; -import { accountUserFactory } from 'src/factories/accountUsers'; +import { paymentMethodFactory } from 'src/factories'; import { grantsFactory } from 'src/factories/grants'; import { renderWithTheme, wrapWithTheme } from 'src/utilities/testHelpers'; @@ -22,32 +21,18 @@ vi.mock('@linode/api-v4/lib/account', async () => { }); const queryMocks = vi.hoisted(() => ({ - useAccountUser: vi.fn().mockReturnValue({}), useGrants: vi.fn().mockReturnValue({}), useProfile: vi.fn().mockReturnValue({}), })); -vi.mock('src/queries/accountUsers', async () => { - const actual = await vi.importActual('src/queries/accountUsers'); - return { - ...actual, - useAccountUser: queryMocks.useAccountUser, - }; -}); - vi.mock('src/queries/profile', async () => { const actual = await vi.importActual('src/queries/profile'); return { ...actual, useGrants: queryMocks.useGrants, - useProfile: queryMocks.useAccountUser, }; }); -queryMocks.useAccountUser.mockReturnValue({ - data: accountUserFactory.build({ user_type: 'parent' }), -}); - /* * Build payment method list that includes 1 valid and default payment method, * 2 valid non-default payment methods, and 1 expired payment method. @@ -64,15 +49,18 @@ const paymentMethods = [ }), ]; +const props = { + isAkamaiCustomer: false, + loading: false, + paymentMethods, + profile: queryMocks.useProfile().data, +}; + describe('Payment Info Panel', () => { it('Shows loading animation when loading', () => { const { getByLabelText } = renderWithTheme( - + ); @@ -82,11 +70,7 @@ describe('Payment Info Panel', () => { it('Shows Add Payment button for Linode customers and hides it for Akamai customers', () => { const { getByTestId, queryByText, rerender } = renderWithTheme( - + ); @@ -95,11 +79,7 @@ describe('Payment Info Panel', () => { rerender( wrapWithTheme( - + ) ); @@ -110,11 +90,7 @@ describe('Payment Info Panel', () => { it('Opens "Add Payment Method" drawer when "Add Payment Method" is clicked', () => { const { getByTestId } = renderWithTheme( - + ); @@ -127,11 +103,7 @@ describe('Payment Info Panel', () => { it('Lists all payment methods for Linode customers', () => { const { getByTestId } = renderWithTheme( - + ); @@ -145,11 +117,7 @@ describe('Payment Info Panel', () => { it('Hides payment methods and shows text for Akamai customers', () => { const { getByTestId, queryByTestId } = renderWithTheme( - + ); @@ -166,19 +134,15 @@ describe('Payment Info Panel', () => { queryMocks.useProfile.mockReturnValue({ data: profileFactory.build({ restricted: false, + user_type: 'child', }), }); - queryMocks.useAccountUser.mockReturnValue({ - data: accountUserFactory.build({ user_type: 'child' }), - }); - const { getByTestId } = renderWithTheme( , { @@ -203,11 +167,7 @@ describe('Payment Info Panel', () => { 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 edfdb3a76cd..fdfc2a6aec6 100644 --- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/PaymentInformation.tsx @@ -12,8 +12,7 @@ import { getDisabledTooltipText } from 'src/features/Billing/billingUtils'; import { ADD_PAYMENT_METHOD } from 'src/features/Billing/constants'; import { useFlags } from 'src/hooks/useFlags'; import { queryKey } from 'src/queries/accountPayment'; -import { useAccountUser } from 'src/queries/accountUsers'; -import { useGrants, useProfile } from 'src/queries/profile'; +import { useGrants } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { @@ -23,15 +22,18 @@ import { } from '../../BillingDetail'; import AddPaymentMethodDrawer from './AddPaymentMethodDrawer'; +import type { Profile } from '@linode/api-v4'; + interface Props { error?: APIError[] | null; isAkamaiCustomer: boolean; loading: boolean; paymentMethods: PaymentMethod[] | undefined; + profile: Profile | undefined; } const PaymentInformation = (props: Props) => { - const { error, isAkamaiCustomer, loading, paymentMethods } = props; + const { error, isAkamaiCustomer, loading, paymentMethods, profile } = props; const [addDrawerOpen, setAddDrawerOpen] = React.useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState( @@ -46,14 +48,13 @@ const PaymentInformation = (props: Props) => { const { replace } = useHistory(); const queryClient = useQueryClient(); const flags = useFlags(); - const { data: profile } = useProfile(); - const { data: user } = useAccountUser(profile?.username ?? ''); const { data: grants } = useGrants(); const drawerLink = '/account/billing/add-payment-method'; const addPaymentMethodRouteMatch = Boolean(useRouteMatch(drawerLink)); const isChildUser = - flags.parentChildAccountAccess && user?.user_type === 'child'; + flags.parentChildAccountAccess && profile?.user_type === 'child'; + const isRestrictedUser = isChildUser || grants?.global.account_access === 'read_only'; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 4a29c8afc2e..c0007de8687 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -550,6 +550,8 @@ export const handlers = [ rest.get('*/profile', (req, res, ctx) => { const profile = profileFactory.build({ restricted: false, + // Parent/Child: switch the `user_type` depending on what account view you need to mock. + user_type: 'parent', }); return res(ctx.json(profile)); }),