From 77403c5729788ca33a48808cc1994903fe58add3 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 8 Nov 2024 17:36:27 +0100 Subject: [PATCH 01/10] feat(shell): add method to notify container a modal action is done ref: MANAGER-15988 Signed-off-by: Jacques Larique --- packages/components/ovh-shell/src/client/api.ts | 5 +++++ .../components/ovh-shell/src/plugin/ux/index.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/components/ovh-shell/src/client/api.ts b/packages/components/ovh-shell/src/client/api.ts index d4856f9a9340..adecf3619196 100644 --- a/packages/components/ovh-shell/src/client/api.ts +++ b/packages/components/ovh-shell/src/client/api.ts @@ -209,6 +209,11 @@ export default function exposeApi(shellClient: ShellClient) { plugin: 'ux', method: 'showPreloader', }), + notifyModalActionDone: () => + shellClient.invokePluginMethod({ + plugin: 'ux', + method: 'notifyModalActionDone', + }), }, navigation: clientNavigation(shellClient), tracking: exposeTrackingAPI(shellClient), diff --git a/packages/components/ovh-shell/src/plugin/ux/index.ts b/packages/components/ovh-shell/src/plugin/ux/index.ts index 8864d457c008..c715be9276cd 100644 --- a/packages/components/ovh-shell/src/plugin/ux/index.ts +++ b/packages/components/ovh-shell/src/plugin/ux/index.ts @@ -20,6 +20,8 @@ export interface IUXPlugin { toggleNotificationsSidebarVisibility(): void; toggleAccountSidebarVisibility(): void; getUserIdCookie(): string; + registerModalActionDoneListener(callback: CallableFunction): void; + notifyModalActionDone(): void; } // TODO: remove this once we have a more generic Plugin class @@ -34,6 +36,8 @@ export class UXPlugin implements IUXPlugin { private sidebarMenuUpdateItemLabelListener?: CallableFunction; + private onModalActionDone?: CallableFunction; + constructor(shell: Shell) { this.shell = shell; @@ -253,4 +257,15 @@ export class UXPlugin implements IUXPlugin { requestClientSidebarOpen() { this.shell.emitEvent('ux:client-sidebar-open'); } + + /* ----------- Modal action methods -----------*/ + registerModalActionDoneListener(callback: CallableFunction) { + this.onModalActionDone = callback; + } + + notifyModalActionDone() { + if (this.onModalActionDone) { + this.onModalActionDone(); + } + } } From ef2796b7eacef32e05547265dda95627326b480f Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 25 Oct 2024 13:49:47 +0200 Subject: [PATCH 02/10] feat(container): added an invitation modal to accept contract ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../apps/container/src/api/agreements.ts | 11 +++ .../apps/container/src/api/authorizations.ts | 23 ++++++ .../AgreementsUpdateModal.component.tsx | 80 +++++++++++++++++++ .../apps/container/src/container/index.tsx | 29 ++++--- .../src/cookie-policy/CookiePolicy.tsx | 9 ++- .../src/hooks/accountUrn/useAccountUrn.tsx | 11 +++ .../hooks/agreements/useAgreementsUpdate.tsx | 12 +++ .../container/src/hooks/modals/useModals.tsx | 19 +++++ .../src/payment-modal/PaymentModal.tsx | 11 ++- .../Messages_fr_FR.json | 5 ++ .../apps/container/src/types/agreements.ts | 6 ++ 11 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 packages/manager/apps/container/src/api/agreements.ts create mode 100644 packages/manager/apps/container/src/api/authorizations.ts create mode 100644 packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx create mode 100644 packages/manager/apps/container/src/hooks/accountUrn/useAccountUrn.tsx create mode 100644 packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx create mode 100644 packages/manager/apps/container/src/hooks/modals/useModals.tsx create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_FR.json create mode 100644 packages/manager/apps/container/src/types/agreements.ts diff --git a/packages/manager/apps/container/src/api/agreements.ts b/packages/manager/apps/container/src/api/agreements.ts new file mode 100644 index 000000000000..5f5d163d8ca5 --- /dev/null +++ b/packages/manager/apps/container/src/api/agreements.ts @@ -0,0 +1,11 @@ +import { fetchIcebergV6, FilterComparator } from "@ovh-ux/manager-core-api"; + +const fetchAgreementsUpdates = async () => { + const { data } = await fetchIcebergV6({ + route: '/me/agreements', + filters: [{ key: 'agreed', comparator: FilterComparator.IsIn, value: ['todo', 'ko'] }], + }); + return data; +}; + +export default fetchAgreementsUpdates; diff --git a/packages/manager/apps/container/src/api/authorizations.ts b/packages/manager/apps/container/src/api/authorizations.ts new file mode 100644 index 000000000000..91e62cb877f6 --- /dev/null +++ b/packages/manager/apps/container/src/api/authorizations.ts @@ -0,0 +1,23 @@ +import { fetchIcebergV2 } from "@ovh-ux/manager-core-api"; + +type IamResource = { + id: string; + urn: string; + name: string; + displayName: string; + type: string; + owner: string; +}; + +export const fetchAccountUrn = async (): Promise => { + const { data } = await fetchIcebergV2({ + route: '/iam/resource?resourceType=account', + }); + /* + const { data } = await v2.get( + '/iam/resource?resourceType=account', + { adapter: 'fetch',fetchOptions: { priority: 'low' } }, + ); + */ + return data[0]?.urn; +}; diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx new file mode 100644 index 000000000000..c9dd450e6eda --- /dev/null +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -0,0 +1,80 @@ +import React, { useContext } from 'react'; +import useAgreementsUpdate from '@/hooks/agreements/useAgreementsUpdate'; +import { ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE, ODS_THEME_COLOR_HUE } from '@ovhcloud/ods-common-theming'; +import { OsdsButton, OsdsModal, OsdsText } from '@ovhcloud/ods-components/react'; +import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; +import ApplicationContext from '@/context'; +import ovhCloudLogo from '@/cookie-policy/assets/logo-ovhcloud.png'; +import { useAuthorizationIam } from '@ovh-ux/manager-react-components/src/hooks/iam'; +import useAccountUrn from '@/hooks/accountUrn/useAccountUrn'; + +export default function AgreementsUpdateModal () { + const { shell } = useContext(ApplicationContext); + const region: string = shell + .getPlugin('environment') + .getEnvironment() + .getRegion(); + const navigation = shell.getPlugin('navigation'); + const { t } = useTranslation('agreements-update-modal'); + const { data: urn } = useAccountUrn({ enabled: region !== 'US' }); + const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); + const { data: agreements } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); + const myContractsLink = navigation.getURL( + 'dedicated', + '#/billing/autoRenew/agreements', + ); + const goToContractPage = () => { + window.top.location.href = myContractsLink; + } + + return agreements?.length ? ( + <> + +
+ ovh-cloud-logo +
+ + {t('agreements_update_modal_title')} + + +

+
+ + + {t('agreements_update_modal_action')} + +
+ + ) : null; +} diff --git a/packages/manager/apps/container/src/container/index.tsx b/packages/manager/apps/container/src/container/index.tsx index e56c26a88084..cebd5aa99dee 100644 --- a/packages/manager/apps/container/src/container/index.tsx +++ b/packages/manager/apps/container/src/container/index.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect, useState } from 'react'; +import React, { Suspense, useEffect } from 'react'; import { Environment } from '@ovh-ux/manager-config'; import LegacyContainer from '@/container/legacy'; @@ -12,6 +12,8 @@ import SSOAuthModal from '@/sso-auth-modal/SSOAuthModal'; import PaymentModal from '@/payment-modal/PaymentModal'; import LiveChat from '@/components/LiveChat'; import { IdentityDocumentsModal } from '@/identity-documents-modal/IdentityDocumentsModal'; +import AgreementsUpdateModal from '@/components/AgreementsUpdateModal/AgreementsUpdateModal.component'; +import useModals from '@/hooks/modals/useModals'; export default function Container(): JSX.Element { const { @@ -23,17 +25,15 @@ export default function Container(): JSX.Element { setChatbotReduced, } = useContainer(); const shell = useShell(); - const [isCookiePolicyApplied, setIsCookiePolicyApplied] = useState(false); const environment: Environment = shell .getPlugin('environment') .getEnvironment(); const language = environment.getUserLanguage(); const { ovhSubsidiary, supportLevel } = environment.getUser(); + const { current, next } = useModals(); const isNavReshuffle = betaVersion && useBeta; - const cookiePolicyHandler = (isApplied: boolean): void => setIsCookiePolicyApplied(isApplied); - useEffect(() => { if (!isLoading) { const tracking = shell.getPlugin('tracking'); @@ -81,19 +81,26 @@ export default function Container(): JSX.Element { - {isCookiePolicyApplied && + {current === 'agreements' && ( - + - } - {isCookiePolicyApplied && + )} + {current === 'payment' && ( + + + + )} + {current === 'payment' && } - - - + {current === 'cookies' && ( + + + + )} ); } diff --git a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx index e78f8fa6edf4..4e039861e13b 100644 --- a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx +++ b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx @@ -24,7 +24,7 @@ import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; type Props = { shell: Shell; - onValidate: Function + onDone: Function }; const ModalContent = ({ label }: { label: string }) => ( @@ -38,7 +38,7 @@ const ModalContent = ({ label }: { label: string }) => ( ); -const CookiePolicy = ({ shell, onValidate }: Props): JSX.Element => { +const CookiePolicy = ({ shell, onDone }: Props): JSX.Element => { const { t } = useTranslation('cookie-policy'); const [cookies, setCookies] = useCookies(['MANAGER_TRACKING']); const { environment } = useApplication(); @@ -64,7 +64,7 @@ const CookiePolicy = ({ shell, onValidate }: Props): JSX.Element => { setCookies('MANAGER_TRACKING', agreed ? 1 : 0); trackingPlugin.onUserConsentFromModal(agreed); setShow(false); - onValidate(true); + onDone(); } useEffect(() => { @@ -73,13 +73,14 @@ const CookiePolicy = ({ shell, onValidate }: Props): JSX.Element => { // activate tracking if region is US or if tracking consent cookie is valid if (isRegionUS || cookies.MANAGER_TRACKING === '1') { trackingPlugin.init(true); + onDone(); } else if (cookies.MANAGER_TRACKING == null) { trackingPlugin.onConsentModalDisplay(); setShow(true); } else { trackingPlugin.setEnabled(false); + onDone(); } - onValidate(isRegionUS || cookies.MANAGER_TRACKING); }, [show]); return ( diff --git a/packages/manager/apps/container/src/hooks/accountUrn/useAccountUrn.tsx b/packages/manager/apps/container/src/hooks/accountUrn/useAccountUrn.tsx new file mode 100644 index 000000000000..0eb8b7cf259a --- /dev/null +++ b/packages/manager/apps/container/src/hooks/accountUrn/useAccountUrn.tsx @@ -0,0 +1,11 @@ +import { DefinedInitialDataOptions, useQuery } from '@tanstack/react-query'; +import { fetchAccountUrn } from '@/api/authorizations'; + +const useAccountUrn = (options?: Partial>) => + useQuery({ + ...options, + queryKey: ['account-urn'], + queryFn: fetchAccountUrn, + }); + +export default useAccountUrn; diff --git a/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx b/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx new file mode 100644 index 000000000000..860f6ef7a4d6 --- /dev/null +++ b/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx @@ -0,0 +1,12 @@ +import { DefinedInitialDataOptions, useQuery } from '@tanstack/react-query'; +import fetchAgreementsUpdates from '@/api/agreements'; +import { Agreements } from '@/types/agreements'; + +const useAgreementsUpdate = (options?: Partial>) => + useQuery({ + ...options, + queryKey: ['agreements'], + queryFn: fetchAgreementsUpdates, + }); + +export default useAgreementsUpdate; diff --git a/packages/manager/apps/container/src/hooks/modals/useModals.tsx b/packages/manager/apps/container/src/hooks/modals/useModals.tsx new file mode 100644 index 000000000000..e64a9691a482 --- /dev/null +++ b/packages/manager/apps/container/src/hooks/modals/useModals.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; + +const modalTypes = [ + 'cookies', + 'payment', + 'agreements', +]; +const useModals = () => { + const [ currentIndex, setCurrentIndex ] = useState(0); + + return { + current: modalTypes[currentIndex], + next: () => { + setCurrentIndex((current) => current + 1) + }, + }; +}; + +export default useModals; diff --git a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx index ec70649f91ff..1ca6ce469d7b 100644 --- a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx +++ b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx @@ -34,7 +34,7 @@ interface IPaymentMethod { } const computeAlert = (paymentMethods: IPaymentMethod[]): string => { - const currentCreditCard: IPaymentMethod = paymentMethods?.find(currentPaymentMethod => currentPaymentMethod.paymentType === 'CREDIT_CARD' + const currentCreditCard: IPaymentMethod = paymentMethods?.find(currentPaymentMethod => currentPaymentMethod.paymentType === 'CREDIT_CARD' && currentPaymentMethod.default); if (currentCreditCard) { @@ -52,7 +52,11 @@ const computeAlert = (paymentMethods: IPaymentMethod[]): string => { return null; }; -const PaymentModal = (): JSX.Element => { +type Props = { + onDone: () => void; +}; + +const PaymentModal = ({ onDone }: Props): JSX.Element => { const [alert, setAlert] = useState(''); const { t } = useTranslation('payment-modal'); const [showPaymentModal, setShowPaymentModal] = useState(false); @@ -80,6 +84,9 @@ const PaymentModal = (): JSX.Element => { setAlert(alert); setShowPaymentModal(true); } + else { + onDone(); + } } }, [paymentResponse]); diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_FR.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_FR.json new file mode 100644 index 000000000000..c651473f474a --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_FR.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "Les conditions de services d'OVHCloud évoluent.", + "agreements_update_modal_description": "Nous vous invitons à en prendre connaissance en cliquant ici", + "agreements_update_modal_action": "Mes contrats" +} diff --git a/packages/manager/apps/container/src/types/agreements.ts b/packages/manager/apps/container/src/types/agreements.ts new file mode 100644 index 000000000000..fc732999d968 --- /dev/null +++ b/packages/manager/apps/container/src/types/agreements.ts @@ -0,0 +1,6 @@ +export type Agreements = { + agreed: boolean; + contractId: number; + date: Date; + id: number; +}; From 46afc298f2922c353a593f2d6703cf5816f9db6a Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 25 Oct 2024 17:46:40 +0200 Subject: [PATCH 03/10] feat(container): add agreements update modal tests ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../images}/logo-ovhcloud.png | Bin .../AgreementsUpdateModal.component.tsx | 4 +- .../AgreementsUpdateModal.spec.tsx | 91 ++++++++++++++++++ .../src/cookie-policy/CookiePolicy.tsx | 2 +- 4 files changed, 95 insertions(+), 2 deletions(-) rename packages/manager/apps/container/src/{cookie-policy/assets => assets/images}/logo-ovhcloud.png (100%) create mode 100644 packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx diff --git a/packages/manager/apps/container/src/cookie-policy/assets/logo-ovhcloud.png b/packages/manager/apps/container/src/assets/images/logo-ovhcloud.png similarity index 100% rename from packages/manager/apps/container/src/cookie-policy/assets/logo-ovhcloud.png rename to packages/manager/apps/container/src/assets/images/logo-ovhcloud.png diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx index c9dd450e6eda..54f7b23838f9 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -5,7 +5,7 @@ import { OsdsButton, OsdsModal, OsdsText } from '@ovhcloud/ods-components/react' import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; import { useTranslation } from 'react-i18next'; import ApplicationContext from '@/context'; -import ovhCloudLogo from '@/cookie-policy/assets/logo-ovhcloud.png'; +import ovhCloudLogo from '@/assets/images/logo-ovhcloud.png'; import { useAuthorizationIam } from '@ovh-ux/manager-react-components/src/hooks/iam'; import useAccountUrn from '@/hooks/accountUrn/useAccountUrn'; @@ -28,12 +28,14 @@ export default function AgreementsUpdateModal () { window.top.location.href = myContractsLink; } + console.log(agreements) return agreements?.length ? ( <>
({ + isAuthorized: false, + region: 'US', + agreements: [], +})); + +const shellContext = { + shell: { + getPlugin: (plugin: string) => { + if (plugin === 'navigation') { + return { + getURL: vi.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve('http://fakelink.com'), 50); + }), + ), + }; + } + return { + getEnvironment: () => ({ + getRegion: vi.fn(() => mocks.region), + }) + }; + }, + } +}; + +const queryClient = new QueryClient(); +const renderComponent = () => { + return render( + + + + + , + ); +}; + +vi.mock('react', async (importOriginal) => { + const module = await importOriginal(); + return { + ...module, + useContext: () => shellContext + } +}); + +vi.mock('@/hooks/accountUrn/useAccountUrn', () => ({ + default: () => () => 'urn' +})); + +vi.mock('@ovh-ux/manager-react-components/src/hooks/iam', () => ({ + useAuthorizationIam: () => () => ({ isAuthorized: mocks.isAuthorized }) +})); + +vi.mock('@/hooks/agreements/useAgreementsUpdate', () => ({ + default: () => ({ data: mocks.agreements }) +})); + +describe('AgreementsUpdateModal', () => { + it('should display nothing for US customers', () => { + const { queryByTestId } = renderComponent(); + expect(queryByTestId('agreements-update-modal')).not.toBeInTheDocument(); + }); + it('should display nothing for non US and non authorized customers', () => { + mocks.region = 'EU'; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('agreements-update-modal')).not.toBeInTheDocument(); + }); + it('should display a modal for non US and authorized customers without new contract', () => { + mocks.isAuthorized = true; + const { queryByTestId } = renderComponent(); + expect(queryByTestId('agreements-update-modal')).not.toBeInTheDocument(); + }); + it('should display a modal for non US and authorized customers', () => { + mocks.agreements.push({ agreed: false, id: 9999, contractId: 9999 }); + const { getByTestId } = renderComponent(); + expect(getByTestId('agreements-update-modal')).not.toBeNull(); + }); +}) diff --git a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx index 4e039861e13b..96bec1a057e9 100644 --- a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx +++ b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx @@ -3,7 +3,7 @@ import { Shell } from '@ovh-ux/shell'; import { useCookies } from 'react-cookie'; import { useTranslation } from 'react-i18next'; import { User } from '@ovh-ux/manager-config'; -import ovhCloudLogo from './assets/logo-ovhcloud.png'; +import ovhCloudLogo from '../assets/images/logo-ovhcloud.png'; import links from './links'; import { useApplication } from '@/context'; import { Subtitle, Links, LinksProps } from '@ovh-ux/manager-react-components'; From 25e78fa80a3f09ec05603f3c5d07f0001b0f0367 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 8 Nov 2024 17:45:55 +0100 Subject: [PATCH 04/10] feat(container): added aggreements modal and reviewed modals display ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 26 +++++++---- .../apps/container/src/container/index.tsx | 44 +++++++++---------- .../src/context/modals/ModalsProvider.tsx | 38 ++++++++++++++++ .../container/src/context/modals/index.ts | 3 ++ .../src/context/modals/modals.context.ts | 15 +++++++ .../src/context/modals/useModals.tsx | 6 +++ .../src/cookie-policy/CookiePolicy.tsx | 9 ++-- .../container/src/hooks/modals/useModals.tsx | 19 -------- .../IdentityDocumentsModal.tsx | 32 ++++++++------ .../src/payment-modal/PaymentModal.tsx | 32 +++++++++----- .../user-identity-documents.controller.js | 28 +++++++++++- .../user-identity-documents.html | 2 +- .../agreements/user-agreements.controller.js | 7 +++ .../billing/src/payment/method/add/routing.js | 7 +++ 14 files changed, 185 insertions(+), 83 deletions(-) create mode 100644 packages/manager/apps/container/src/context/modals/ModalsProvider.tsx create mode 100644 packages/manager/apps/container/src/context/modals/index.ts create mode 100644 packages/manager/apps/container/src/context/modals/modals.context.ts create mode 100644 packages/manager/apps/container/src/context/modals/useModals.tsx delete mode 100644 packages/manager/apps/container/src/hooks/modals/useModals.tsx diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx index 54f7b23838f9..b6fcb26e7fa5 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -1,6 +1,6 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import useAgreementsUpdate from '@/hooks/agreements/useAgreementsUpdate'; -import { ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE, ODS_THEME_COLOR_HUE } from '@ovhcloud/ods-common-theming'; +import { ODS_THEME_COLOR_HUE, ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE } from '@ovhcloud/ods-common-theming'; import { OsdsButton, OsdsModal, OsdsText } from '@ovhcloud/ods-components/react'; import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; import { useTranslation } from 'react-i18next'; @@ -8,6 +8,8 @@ import ApplicationContext from '@/context'; import ovhCloudLogo from '@/assets/images/logo-ovhcloud.png'; import { useAuthorizationIam } from '@ovh-ux/manager-react-components/src/hooks/iam'; import useAccountUrn from '@/hooks/accountUrn/useAccountUrn'; +import { ModalTypes } from '@/context/modals/modals.context'; +import { useModals } from '@/context/modals'; export default function AgreementsUpdateModal () { const { shell } = useContext(ApplicationContext); @@ -16,19 +18,25 @@ export default function AgreementsUpdateModal () { .getEnvironment() .getRegion(); const navigation = shell.getPlugin('navigation'); - const { t } = useTranslation('agreements-update-modal'); - const { data: urn } = useAccountUrn({ enabled: region !== 'US' }); - const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); - const { data: agreements } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); + const { current } = useModals(); const myContractsLink = navigation.getURL( 'dedicated', '#/billing/autoRenew/agreements', ); + const { t } = useTranslation('agreements-update-modal'); + const { data: urn } = useAccountUrn({ enabled: region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink }); + const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); + const { data: agreements, isLoading } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { - window.top.location.href = myContractsLink; - } + navigation.navigateTo('dedicated', `#/billing/autoRenew/agreements`); + }; + + useEffect(() => { + if (canUserAcceptAgreements && !agreements?.length && current === ModalTypes.agreements) { + shell.getPlugin('ux').notifyModalActionDone(); + } + }, [canUserAcceptAgreements, agreements, current]); - console.log(agreements) return agreements?.length ? ( <> setIsCookiePolicyApplied(isApplied); + useEffect(() => { if (!isLoading) { const tracking = shell.getPlugin('tracking'); @@ -81,26 +85,22 @@ export default function Container(): JSX.Element { - {current === 'agreements' && ( - - - - )} - {current === 'payment' && ( - - - - )} - {current === 'payment' && - - - - } - {current === 'cookies' && ( - - - + {isCookiePolicyApplied && ( + + + + + + + + + + + )} + + + ); } diff --git a/packages/manager/apps/container/src/context/modals/ModalsProvider.tsx b/packages/manager/apps/container/src/context/modals/ModalsProvider.tsx new file mode 100644 index 000000000000..ec9a468dcf5a --- /dev/null +++ b/packages/manager/apps/container/src/context/modals/ModalsProvider.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from 'react'; + +import ModalsContext, { ModalsContextType, ModalTypes } from './modals.context'; + +import { useShell } from '@/context'; + +type Props = { + children: JSX.Element | JSX.Element[]; +}; + +export const ModalsProvider = ({ children = null }: Props): JSX.Element => { + const shell = useShell(); + const uxPlugin = shell.getPlugin('ux'); + const [current, setCurrent] = useState(ModalTypes.kyc); + + useEffect(() => { + uxPlugin.registerModalActionDoneListener(() => { + setCurrent((previous) => { + if (previous === null) { + return null; + } + return (previous < ModalTypes.agreements) ? (previous + 1 as ModalTypes) : null; + }); + }); + }, []); + + const modalsContext: ModalsContextType = { + current, + }; + + return ( + + {children} + + ); +}; + +export default ModalsProvider; diff --git a/packages/manager/apps/container/src/context/modals/index.ts b/packages/manager/apps/container/src/context/modals/index.ts new file mode 100644 index 000000000000..0604907c0fc9 --- /dev/null +++ b/packages/manager/apps/container/src/context/modals/index.ts @@ -0,0 +1,3 @@ +export * from './ModalsProvider'; + +export { default as useModals } from './useModals'; diff --git a/packages/manager/apps/container/src/context/modals/modals.context.ts b/packages/manager/apps/container/src/context/modals/modals.context.ts new file mode 100644 index 000000000000..a09fcc764ad8 --- /dev/null +++ b/packages/manager/apps/container/src/context/modals/modals.context.ts @@ -0,0 +1,15 @@ +import { createContext } from 'react'; + +export enum ModalTypes { + kyc, + payment, + agreements, +} + +export type ModalsContextType = { + current: ModalTypes; +}; + +const ModalsContext = createContext({} as ModalsContextType); + +export default ModalsContext; diff --git a/packages/manager/apps/container/src/context/modals/useModals.tsx b/packages/manager/apps/container/src/context/modals/useModals.tsx new file mode 100644 index 000000000000..811dd70ac9ce --- /dev/null +++ b/packages/manager/apps/container/src/context/modals/useModals.tsx @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import ModalsContext from '@/context/modals/modals.context'; + +const useModals = () => useContext(ModalsContext); + +export default useModals; diff --git a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx index 96bec1a057e9..1a64e947a49c 100644 --- a/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx +++ b/packages/manager/apps/container/src/cookie-policy/CookiePolicy.tsx @@ -24,7 +24,7 @@ import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; type Props = { shell: Shell; - onDone: Function + onValidate: Function }; const ModalContent = ({ label }: { label: string }) => ( @@ -38,7 +38,7 @@ const ModalContent = ({ label }: { label: string }) => ( ); -const CookiePolicy = ({ shell, onDone }: Props): JSX.Element => { +const CookiePolicy = ({ shell, onValidate }: Props): JSX.Element => { const { t } = useTranslation('cookie-policy'); const [cookies, setCookies] = useCookies(['MANAGER_TRACKING']); const { environment } = useApplication(); @@ -64,7 +64,7 @@ const CookiePolicy = ({ shell, onDone }: Props): JSX.Element => { setCookies('MANAGER_TRACKING', agreed ? 1 : 0); trackingPlugin.onUserConsentFromModal(agreed); setShow(false); - onDone(); + onValidate(); } useEffect(() => { @@ -73,14 +73,13 @@ const CookiePolicy = ({ shell, onDone }: Props): JSX.Element => { // activate tracking if region is US or if tracking consent cookie is valid if (isRegionUS || cookies.MANAGER_TRACKING === '1') { trackingPlugin.init(true); - onDone(); } else if (cookies.MANAGER_TRACKING == null) { trackingPlugin.onConsentModalDisplay(); setShow(true); } else { trackingPlugin.setEnabled(false); - onDone(); } + onValidate(isRegionUS || cookies.MANAGER_TRACKING); }, [show]); return ( diff --git a/packages/manager/apps/container/src/hooks/modals/useModals.tsx b/packages/manager/apps/container/src/hooks/modals/useModals.tsx deleted file mode 100644 index e64a9691a482..000000000000 --- a/packages/manager/apps/container/src/hooks/modals/useModals.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useState } from 'react'; - -const modalTypes = [ - 'cookies', - 'payment', - 'agreements', -]; -const useModals = () => { - const [ currentIndex, setCurrentIndex ] = useState(0); - - return { - current: modalTypes[currentIndex], - next: () => { - setCurrentIndex((current) => current + 1) - }, - }; -}; - -export default useModals; diff --git a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx index a014718ed3e4..4ddc87d49bf6 100644 --- a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx +++ b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx @@ -1,29 +1,26 @@ import { - kycIndiaModalLocalStorageKey, kycIndiaFeature, + kycIndiaModalLocalStorageKey, requiredStatusKey, trackingContext, trackingPrefix, } from './constants'; import { useIdentityDocumentsStatus } from '@/hooks/useIdentityDocumentsStatus'; import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; -import { FunctionComponent, useEffect, useRef, useState } from 'react'; +import { FunctionComponent, useEffect, useMemo, useRef, useState } from 'react'; import { useFeatureAvailability } from '@ovh-ux/manager-react-components'; -import { useTranslation, Trans } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useLocalStorage } from 'react-use'; import { useShell } from '@/context'; -import { - OsdsButton, - OsdsCollapsible, - OsdsModal, - OsdsText, -} from '@ovhcloud/ods-components/react'; +import { OsdsButton, OsdsCollapsible, OsdsModal, OsdsText, } from '@ovhcloud/ods-components/react'; import { ODS_THEME_COLOR_HUE, ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_LEVEL, ODS_THEME_TYPOGRAPHY_SIZE, } from '@ovhcloud/ods-common-theming'; +import { useModals } from '@/context/modals'; +import { ModalTypes } from '@/context/modals/modals.context'; export const IdentityDocumentsModal: FunctionComponent = () => { const shell = useShell(); @@ -31,17 +28,21 @@ export const IdentityDocumentsModal: FunctionComponent = () => { const [storage, setStorage] = useLocalStorage( kycIndiaModalLocalStorageKey, ); + const { current } = useModals(); + + const kycURL = navigationPlugin.getURL('dedicated', `#/identity-documents`); const { t } = useTranslation('identity-documents-modal'); const legalInformationRef = useRef(null); const [showModal, setShowModal] = useState(false); - const availabilityDataResponse = useFeatureAvailability([kycIndiaFeature]); - const availability = availabilityDataResponse?.data; + const { data: availability, isLoading: isFeatureAvailabilityLoading } = useFeatureAvailability([kycIndiaFeature]); + + const isKycAvailable = useMemo(() => Boolean(availability && availability[kycIndiaFeature] && !storage), [availability, storage]); const { data: statusDataResponse } = useIdentityDocumentsStatus({ - enabled: Boolean(availability && availability[kycIndiaFeature] && !storage), + enabled: isKycAvailable && current === ModalTypes.kyc && window.location.href !== kycURL, }); const trackingPlugin = shell.getPlugin('tracking'); @@ -57,7 +58,6 @@ export const IdentityDocumentsModal: FunctionComponent = () => { const onConfirm = () => { setShowModal(false); - setStorage(true); trackingPlugin.trackClick({ name: `${trackingPrefix}::pop-up::button::kyc::start-verification`, type: 'action', @@ -66,6 +66,12 @@ export const IdentityDocumentsModal: FunctionComponent = () => { navigationPlugin.navigateTo('dedicated', `#/identity-documents`); }; + useEffect(() => { + if (!isFeatureAvailabilityLoading && !isKycAvailable && current === ModalTypes.kyc) { + shell.getPlugin('ux').notifyModalActionDone(); + } + }, [isFeatureAvailabilityLoading, isKycAvailable, current]); + useEffect(() => { if (statusDataResponse?.data?.status === requiredStatusKey) { setShowModal(true); diff --git a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx index 1ca6ce469d7b..89eeffa58251 100644 --- a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx +++ b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx @@ -15,6 +15,8 @@ import { ODS_THEME_TYPOGRAPHY_SIZE, } from '@ovhcloud/ods-common-theming'; import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; +import { useModals } from '@/context/modals'; +import { ModalTypes } from '@/context/modals/modals.context'; interface IPaymentMethod { icon?: any; @@ -52,29 +54,32 @@ const computeAlert = (paymentMethods: IPaymentMethod[]): string => { return null; }; -type Props = { - onDone: () => void; -}; - -const PaymentModal = ({ onDone }: Props): JSX.Element => { +const PaymentModal = (): JSX.Element => { const [alert, setAlert] = useState(''); const { t } = useTranslation('payment-modal'); const [showPaymentModal, setShowPaymentModal] = useState(false); const shell = useShell(); + const { current } = useModals(); const paymentMethodURL = shell .getPlugin('navigation') .getURL('dedicated', '#/billing/payment/method'); - const closeHandler = () => setShowPaymentModal(false); + const closeHandler = () => { + setShowPaymentModal(false); + shell.getPlugin('ux').notifyModalActionDone(); + }; const validateHandler = () => { setShowPaymentModal(false); window.location.href = paymentMethodURL; - } + }; + + const isReadyToRequest = current === ModalTypes.payment && window.location.href !== paymentMethodURL; - const { data: paymentResponse } = useQuery({ + const { data: paymentResponse, isLoading } = useQuery({ queryKey: ['me-payment-method'], - queryFn: () => fetchIcebergV6({ route: '/me/payment/method' }) + queryFn: () => fetchIcebergV6({ route: '/me/payment/method' }), + enabled: isReadyToRequest, }); useEffect(() => { @@ -84,11 +89,14 @@ const PaymentModal = ({ onDone }: Props): JSX.Element => { setAlert(alert); setShowPaymentModal(true); } - else { - onDone(); + else if (isReadyToRequest) { + shell.getPlugin('ux').notifyModalActionDone(); } } - }, [paymentResponse]); + else if (isReadyToRequest && !isLoading) { + shell.getPlugin('ux').notifyModalActionDone(); + } + }, [paymentResponse, isReadyToRequest, isLoading]); return !showPaymentModal ? ( <> diff --git a/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.controller.js b/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.controller.js index 8ed23ba0e592..df68d945b818 100644 --- a/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.controller.js +++ b/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.controller.js @@ -25,7 +25,16 @@ const replaceTrackingParams = (hit, params) => { export default class AccountUserIdentityDocumentsController { /* @ngInject */ - constructor($q, $http, $scope, coreConfig, coreURLBuilder, atInternet) { + constructor( + $injector, + $q, + $http, + $scope, + coreConfig, + coreURLBuilder, + atInternet, + ) { + this.$injector = $injector; this.$q = $q; this.$http = $http; this.$scope = $scope; @@ -63,6 +72,9 @@ export default class AccountUserIdentityDocumentsController { this.proofs = this.DOCUMENTS_MATRIX[this.user_type]?.proofs; this.selectProofType(null); this.trackPage(TRACKING_TASK_TAG.dashboard); + // We are storing the information that the KYC India modal validation has been displayed, that way we won't + // display it on the next connection + localStorage.setItem('KYC_INDIA_IDENTITY_DOCUMENTS_MODAL', 'true'); } selectProofType(proof) { @@ -100,7 +112,9 @@ export default class AccountUserIdentityDocumentsController { this.tryToFinalizeProcedure(this.links) : // In order to start the KYC procedure we need to request the upload links for the number of documents // the user wants to upload - this.getUploadDocumentsLinks(Object.values(this.files).flatMap(({ files }) => files).length) + this.getUploadDocumentsLinks( + Object.values(this.files).flatMap(({ files }) => files).length, + ) // Once we retrieved the upload links, we'll try to upload them and then "finalize" the procedure creation .then(({ data: { uploadLinks } }) => { this.links = uploadLinks; @@ -135,6 +149,16 @@ export default class AccountUserIdentityDocumentsController { this.isOpenInformationModal = open; } + closeInformationModal() { + this.handleInformationModal(false); + // We try to notify the container that the action required by the KYCIndiaModal has been done + // and we can switch to the next one if necessary + if (this.$injector.has('shellClient')) { + const shellClient = this.$injector.get('shellClient'); + shellClient.ux.notifyModalActionDone(); + } + } + addDocuments(proofType, documentType, files, isReset) { if (isReset) { delete this.files[proofType]; diff --git a/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.html b/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.html index 196ac106f4a8..246b02d7a958 100644 --- a/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.html +++ b/packages/manager/apps/dedicated/client/app/account/identity-documents/user-identity-documents.html @@ -166,6 +166,6 @@ diff --git a/packages/manager/modules/billing/src/autoRenew/agreements/user-agreements.controller.js b/packages/manager/modules/billing/src/autoRenew/agreements/user-agreements.controller.js index 01343c9a50c9..8562bcdbd49d 100644 --- a/packages/manager/modules/billing/src/autoRenew/agreements/user-agreements.controller.js +++ b/packages/manager/modules/billing/src/autoRenew/agreements/user-agreements.controller.js @@ -1,6 +1,7 @@ import get from 'lodash/get'; export default /* @ngInject */ function UserAccountAgreementsController( + $injector, $scope, $translate, Alerter, @@ -77,6 +78,12 @@ export default /* @ngInject */ function UserAccountAgreementsController( UserAccountServicesAgreements.accept(contract) .then( () => { + // After the last contract has been accepted, we'll try to indicate to the container, that agreements updates + // have been accepted + if ($injector.has('shellClient')) { + const shellClient = $injector.get('shellClient'); + shellClient.ux.notifyModalActionDone(); + } $scope.getToValidate(); $scope.$broadcast('paginationServerSide.reload', 'agreementsList'); }, diff --git a/packages/manager/modules/billing/src/payment/method/add/routing.js b/packages/manager/modules/billing/src/payment/method/add/routing.js index e040a84d8963..6e728af3a7f3 100644 --- a/packages/manager/modules/billing/src/payment/method/add/routing.js +++ b/packages/manager/modules/billing/src/payment/method/add/routing.js @@ -128,6 +128,7 @@ export default /* @ngInject */ ($stateProvider, $urlRouterProvider) => { $transition$.params().redirectResult, onPaymentMethodAdded: /* @ngInject */ ( $transition$, + $injector, $translate, goPaymentList, RedirectionService, @@ -138,6 +139,12 @@ export default /* @ngInject */ ($stateProvider, $urlRouterProvider) => { window.location.href = callbackUrl; return callbackUrl; } + // We try to notify the container that the action required by the PaymentModal has been done + // and we can switch to the next one if necessary + if ($injector.has('shellClient')) { + const shellClient = $injector.get('shellClient'); + shellClient.ux.notifyModalActionDone(); + } return goPaymentList( { From a3479a6dda34c1c89e61874168a289860c688484 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Tue, 12 Nov 2024 10:49:41 +0100 Subject: [PATCH 05/10] feat(container): added aggreements modal and reviewed modals display ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../IdentityDocumentsModal.tsx | 1 + .../container/src/payment-modal/PaymentModal.tsx | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx index 4ddc87d49bf6..e20edcd25c99 100644 --- a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx +++ b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx @@ -52,6 +52,7 @@ export const IdentityDocumentsModal: FunctionComponent = () => { setStorage(true); trackingPlugin.trackClick({ name: `${trackingPrefix}::pop-up::link::kyc::cancel`, + type: 'action', ...trackingContext, }); }; diff --git a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx index 89eeffa58251..846410156c82 100644 --- a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx +++ b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx @@ -35,8 +35,8 @@ interface IPaymentMethod { paymentMethodId: number; } -const computeAlert = (paymentMethods: IPaymentMethod[]): string => { - const currentCreditCard: IPaymentMethod = paymentMethods?.find(currentPaymentMethod => currentPaymentMethod.paymentType === 'CREDIT_CARD' +const computeAlert = (paymentMethods: IPaymentMethod[] =[]): string => { + const currentCreditCard: IPaymentMethod = paymentMethods.find(currentPaymentMethod => currentPaymentMethod.paymentType === 'CREDIT_CARD' && currentPaymentMethod.default); if (currentCreditCard) { @@ -83,19 +83,16 @@ const PaymentModal = (): JSX.Element => { }); useEffect(() => { - if (paymentResponse) { - const alert = computeAlert(paymentResponse.data); + if (isReadyToRequest && !isLoading) { + const alert = computeAlert(paymentResponse?.data); if (alert) { setAlert(alert); setShowPaymentModal(true); } - else if (isReadyToRequest) { + else { shell.getPlugin('ux').notifyModalActionDone(); } } - else if (isReadyToRequest && !isLoading) { - shell.getPlugin('ux').notifyModalActionDone(); - } }, [paymentResponse, isReadyToRequest, isLoading]); return !showPaymentModal ? ( From 87b4755da1255f118a2fd88c260e0635e723c63b Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Wed, 13 Nov 2024 15:23:33 +0100 Subject: [PATCH 06/10] feat(container): added aggreements modal and reviewed modals display ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal/AgreementsUpdateModal.component.tsx | 2 +- packages/manager/apps/container/src/container/index.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx index b6fcb26e7fa5..2215c8d1728a 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -26,7 +26,7 @@ export default function AgreementsUpdateModal () { const { t } = useTranslation('agreements-update-modal'); const { data: urn } = useAccountUrn({ enabled: region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink }); const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); - const { data: agreements, isLoading } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); + const { data: agreements } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { navigation.navigateTo('dedicated', `#/billing/autoRenew/agreements`); }; diff --git a/packages/manager/apps/container/src/container/index.tsx b/packages/manager/apps/container/src/container/index.tsx index bcbdf9c2ba30..2ac8148d14e3 100644 --- a/packages/manager/apps/container/src/container/index.tsx +++ b/packages/manager/apps/container/src/container/index.tsx @@ -25,7 +25,6 @@ export default function Container(): JSX.Element { chatbotReduced, setChatbotReduced, } = useContainer(); - useModals(); const shell = useShell(); const [isCookiePolicyApplied, setIsCookiePolicyApplied] = useState(false); const environment: Environment = shell From bd620ea7279654fa04841954e5153e32ab786d11 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Thu, 14 Nov 2024 15:28:59 +0100 Subject: [PATCH 07/10] feat(container): remove unnecessary comment ref: MANAGER-14722 Signed-off-by: Jacques Larique --- packages/manager/apps/container/src/api/authorizations.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/manager/apps/container/src/api/authorizations.ts b/packages/manager/apps/container/src/api/authorizations.ts index 91e62cb877f6..d3d11208fdb0 100644 --- a/packages/manager/apps/container/src/api/authorizations.ts +++ b/packages/manager/apps/container/src/api/authorizations.ts @@ -13,11 +13,6 @@ export const fetchAccountUrn = async (): Promise => { const { data } = await fetchIcebergV2({ route: '/iam/resource?resourceType=account', }); - /* - const { data } = await v2.get( - '/iam/resource?resourceType=account', - { adapter: 'fetch',fetchOptions: { priority: 'low' } }, - ); - */ + return data[0]?.urn; }; From 74ed2b252f0667dcc5afe442a8e26c68503ae473 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 11:37:33 +0100 Subject: [PATCH 08/10] feat(container): fixed typo in agreements update modal redirection url ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal/AgreementsUpdateModal.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx index 2215c8d1728a..6a51f9ad34eb 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -28,7 +28,7 @@ export default function AgreementsUpdateModal () { const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); const { data: agreements } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { - navigation.navigateTo('dedicated', `#/billing/autoRenew/agreements`); + navigation.navigateTo('dedicated', `#/billing/autorenew/agreements`); }; useEffect(() => { From c774dc1f28b3ecf15757ccafd78df1bf259cd4d6 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 13:16:08 +0100 Subject: [PATCH 09/10] feat(container): fixed agreements update modal display management ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 61 +++++++++++++------ .../Messages_fr_FR.json | 2 +- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx index 6a51f9ad34eb..a960120552f0 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -1,15 +1,16 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import useAgreementsUpdate from '@/hooks/agreements/useAgreementsUpdate'; import { ODS_THEME_COLOR_HUE, ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE } from '@ovhcloud/ods-common-theming'; -import { OsdsButton, OsdsModal, OsdsText } from '@ovhcloud/ods-components/react'; +import { OsdsButton, OsdsLink, OsdsModal, OsdsText } from '@ovhcloud/ods-components/react'; import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT, ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import ApplicationContext from '@/context'; import ovhCloudLogo from '@/assets/images/logo-ovhcloud.png'; import { useAuthorizationIam } from '@ovh-ux/manager-react-components/src/hooks/iam'; import useAccountUrn from '@/hooks/accountUrn/useAccountUrn'; import { ModalTypes } from '@/context/modals/modals.context'; import { useModals } from '@/context/modals'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; export default function AgreementsUpdateModal () { const { shell } = useContext(ApplicationContext); @@ -21,23 +22,38 @@ export default function AgreementsUpdateModal () { const { current } = useModals(); const myContractsLink = navigation.getURL( 'dedicated', - '#/billing/autoRenew/agreements', + '#/billing/autorenew/agreements', ); + const [ showModal, setShowModal ] = useState(false); + const shouldTryToDisplay = useMemo(() => region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink, [region, current, window.location.href]); const { t } = useTranslation('agreements-update-modal'); - const { data: urn } = useAccountUrn({ enabled: region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink }); - const { isAuthorized: canUserAcceptAgreements } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); - const { data: agreements } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); + const { data: urn } = useAccountUrn({ enabled: shouldTryToDisplay }); + const { isAuthorized: canUserAcceptAgreements, isLoading: isAuthorizationLoading } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); + const { data: agreements, isLoading: areAgreementsLoading } = useAgreementsUpdate({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { + setShowModal(false); navigation.navigateTo('dedicated', `#/billing/autorenew/agreements`); }; useEffect(() => { - if (canUserAcceptAgreements && !agreements?.length && current === ModalTypes.agreements) { - shell.getPlugin('ux').notifyModalActionDone(); + // We consider we have loaded all information if conditions are not respected to try to display the modal + const hasFullyLoaded = !shouldTryToDisplay + // Or authorization are loaded but user does have right to accept contract or has no contract to accept + || !isAuthorizationLoading && (!canUserAcceptAgreements || !areAgreementsLoading); + // If current modal to be displayed is the agreements one and everything has loaded we can handle the display + if (shouldTryToDisplay && hasFullyLoaded) { + // If no contract are to be accepted we go to the next modal (if it exists) + if (!agreements?.length) { + shell.getPlugin('ux').notifyModalActionDone(); + } + // Otherwise we display the modal + else { + setShowModal(true); + } } - }, [canUserAcceptAgreements, agreements, current]); + }, [shouldTryToDisplay, canUserAcceptAgreements, agreements, current]); - return agreements?.length ? ( + return showModal ? ( <> -

+

+ setShowModal(false)} + > + ), + }} + /> +

ici", + "agreements_update_modal_description": "Nous vous invitons à en prendre connaissance en cliquant ici", "agreements_update_modal_action": "Mes contrats" } From aa06630b2c8dd2ab3ce908e73cc57ddb38685051 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 14:04:47 +0100 Subject: [PATCH 10/10] feat(container): fixed agreements update modal tests ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.spec.tsx | 24 ++++++++++++------- .../manager/apps/container/src/setupTests.ts | 3 ++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx index 51300f38850a..afacb68c6bc3 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx @@ -6,6 +6,7 @@ import { ShellContextType, } from '@ovh-ux/manager-react-shell-client'; import AgreementsUpdateModal from '@/components/AgreementsUpdateModal/AgreementsUpdateModal.component'; +import { ModalTypes } from '@/context/modals/modals.context'; const mocks = vi.hoisted(() => ({ isAuthorized: false, @@ -16,8 +17,8 @@ const mocks = vi.hoisted(() => ({ const shellContext = { shell: { getPlugin: (plugin: string) => { - if (plugin === 'navigation') { - return { + switch (plugin) { + case 'navigation': return { getURL: vi.fn( () => new Promise((resolve) => { @@ -25,12 +26,15 @@ const shellContext = { }), ), }; + case 'ux': return { + notifyModalActionDone: vi.fn(), + }; + case 'environment': return { + getEnvironment: () => ({ + getRegion: vi.fn(() => mocks.region), + }) + }; } - return { - getEnvironment: () => ({ - getRegion: vi.fn(() => mocks.region), - }) - }; }, } }; @@ -64,6 +68,10 @@ vi.mock('@ovh-ux/manager-react-components/src/hooks/iam', () => ({ useAuthorizationIam: () => () => ({ isAuthorized: mocks.isAuthorized }) })); +vi.mock('@/context/modals', () => ({ + useModals: () => ({ current: ModalTypes.agreements }) +})); + vi.mock('@/hooks/agreements/useAgreementsUpdate', () => ({ default: () => ({ data: mocks.agreements }) })); @@ -78,7 +86,7 @@ describe('AgreementsUpdateModal', () => { const { queryByTestId } = renderComponent(); expect(queryByTestId('agreements-update-modal')).not.toBeInTheDocument(); }); - it('should display a modal for non US and authorized customers without new contract', () => { + it('should display nothing for non US and authorized customers without new contract', () => { mocks.isAuthorized = true; const { queryByTestId } = renderComponent(); expect(queryByTestId('agreements-update-modal')).not.toBeInTheDocument(); diff --git a/packages/manager/apps/container/src/setupTests.ts b/packages/manager/apps/container/src/setupTests.ts index aaf6fb6b73e9..046cb394b948 100644 --- a/packages/manager/apps/container/src/setupTests.ts +++ b/packages/manager/apps/container/src/setupTests.ts @@ -11,4 +11,5 @@ vi.mock('react-i18next', () => ({ changeLanguage: () => new Promise(() => {}), }, }), -})); \ No newline at end of file + Trans: ({ children }: { children: React.ReactNode }) => children, +}));