From 4795865b7ac124ee710660fc76f485028a53cce8 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 25 Oct 2024 13:49:47 +0200 Subject: [PATCH 01/22] 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 | 9 ++- .../Messages_fr_FR.json | 5 ++ .../apps/container/src/types/agreements.ts | 6 ++ 11 files changed, 198 insertions(+), 16 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 d044ad74870b..35e95df48135 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 107be967efaf..503beece32a5 100644 --- a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx +++ b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx @@ -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 4b432031798893bf5d70f37bbacab065def7cfdc Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 25 Oct 2024 17:46:40 +0200 Subject: [PATCH 02/22] 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 35e95df48135..a93014834e7c 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 b5b23338215c95a1c4e66e7c965e57120eb5d97e Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 8 Nov 2024 17:45:55 +0100 Subject: [PATCH 03/22] 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 a93014834e7c..fa03a4c90384 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 503beece32a5..d23385a57310 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 697fbb262c8011debee2d9d1c9e25ddc586e060c Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Tue, 12 Nov 2024 10:49:41 +0100 Subject: [PATCH 04/22] 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 d23385a57310..45f4ab98f4e3 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?.expirationDate) { @@ -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 87404d3db3fb46fe8e6fec9f47d25c0a56bf4bd4 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Wed, 13 Nov 2024 15:23:33 +0100 Subject: [PATCH 05/22] 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 5d980d4ec9be4a698f6719d0b8066e902a270ba1 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Thu, 14 Nov 2024 15:28:59 +0100 Subject: [PATCH 06/22] 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 4f5fe6d0f7c345c0a553fdd28ba81df6f555ca0d Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 11:37:33 +0100 Subject: [PATCH 07/22] 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 a002e3430dac0c54d5f643cf6c8f1cd87764b60d Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 13:16:08 +0100 Subject: [PATCH 08/22] 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 4a216a2cc2390a3025b659ebbc3c760f6eab7011 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 15 Nov 2024 14:04:47 +0100 Subject: [PATCH 09/22] 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, +})); From 236c8559c9add37f2d8a3a57ae204a807515ccd9 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Mon, 18 Nov 2024 11:51:47 +0100 Subject: [PATCH 10/22] feat(container): fixed kyc modal management ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../IdentityDocumentsModal.tsx | 27 ++++++++++++------- .../Messages_fr_FR.json | 4 +-- 2 files changed, 19 insertions(+), 12 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 e20edcd25c99..27c7f1597d95 100644 --- a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx +++ b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx @@ -41,7 +41,7 @@ export const IdentityDocumentsModal: FunctionComponent = () => { const isKycAvailable = useMemo(() => Boolean(availability && availability[kycIndiaFeature] && !storage), [availability, storage]); - const { data: statusDataResponse } = useIdentityDocumentsStatus({ + const { data: statusDataResponse, isLoading: isProcedureStatusLoading } = useIdentityDocumentsStatus({ enabled: isKycAvailable && current === ModalTypes.kyc && window.location.href !== kycURL, }); @@ -68,16 +68,23 @@ export const IdentityDocumentsModal: FunctionComponent = () => { }; useEffect(() => { - if (!isFeatureAvailabilityLoading && !isKycAvailable && current === ModalTypes.kyc) { - shell.getPlugin('ux').notifyModalActionDone(); + const shouldTryToDisplay = isKycAvailable && current === ModalTypes.kyc && window.location.href !== kycURL; + // We consider we have loaded all information if conditions are not respected to try to display the modal + const hasFullyLoaded = !shouldTryToDisplay + // Or procedure status is loaded + || !isProcedureStatusLoading; + // If current modal to be displayed is the kyc one and everything has loaded we can handle the display + if (shouldTryToDisplay && hasFullyLoaded) { + // If procedure's status is required we display the modal + if (statusDataResponse?.data?.status === requiredStatusKey) { + setShowModal(true); + } + // Otherwise we go to the next modal (if it exists) + else { + shell.getPlugin('ux').notifyModalActionDone(); + } } - }, [isFeatureAvailabilityLoading, isKycAvailable, current]); - - useEffect(() => { - if (statusDataResponse?.data?.status === requiredStatusKey) { - setShowModal(true); - } - }, [statusDataResponse?.data?.status]); + }, [isKycAvailable, current, isProcedureStatusLoading, statusDataResponse]); useEffect(() => { if (showModal) { 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 index 7cf93673320f..df8c84cbfef3 100644 --- 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 @@ -1,5 +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_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" } From 62a2c44afe22442a628f681ad6fdce4e96ba7d43 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Wed, 27 Nov 2024 10:44:23 +0100 Subject: [PATCH 11/22] feat(container): fixed agreements and kyc india modals workflows ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 28 ++++++++------ .../IdentityDocumentsModal.tsx | 38 +++++++++++-------- 2 files changed, 40 insertions(+), 26 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 a960120552f0..e7d2e7089eab 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -11,6 +11,7 @@ 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'; +import { requiredStatusKey } from '@/identity-documents-modal/constants'; export default function AgreementsUpdateModal () { const { shell } = useContext(ApplicationContext); @@ -36,22 +37,27 @@ export default function AgreementsUpdateModal () { }; useEffect(() => { + const shouldManageModal = region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink; // We consider we have loaded all information if conditions are not respected to try to display the modal - const hasFullyLoaded = !shouldTryToDisplay + const hasFullyLoaded = !shouldManageModal // 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); + // We handle the modal display only when the Agreements modal is the current one, contract management is available + // (region is not US) and we are not on the page where the user can do the related action (accepts his contract) + if (shouldManageModal) { + // We will wait for all data to be retrieved before handling the modal lifecycle + if (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); + } } } - }, [shouldTryToDisplay, canUserAcceptAgreements, agreements, current]); + }, [canUserAcceptAgreements, isAuthorizationLoading, canUserAcceptAgreements, areAgreementsLoading, agreements, current]); return showModal ? ( <> 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 27c7f1597d95..1053112ee6b2 100644 --- a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx +++ b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx @@ -68,23 +68,31 @@ export const IdentityDocumentsModal: FunctionComponent = () => { }; useEffect(() => { - const shouldTryToDisplay = isKycAvailable && current === ModalTypes.kyc && window.location.href !== kycURL; - // We consider we have loaded all information if conditions are not respected to try to display the modal - const hasFullyLoaded = !shouldTryToDisplay - // Or procedure status is loaded - || !isProcedureStatusLoading; - // If current modal to be displayed is the kyc one and everything has loaded we can handle the display - if (shouldTryToDisplay && hasFullyLoaded) { - // If procedure's status is required we display the modal - if (statusDataResponse?.data?.status === requiredStatusKey) { - setShowModal(true); - } - // Otherwise we go to the next modal (if it exists) - else { - shell.getPlugin('ux').notifyModalActionDone(); + const shouldManageModal = current === ModalTypes.kyc && window.location.href !== kycURL; + // We handle the modal display only when the KYC modal is the current one, and we are not on the page + // where the user can do the related action (upload his documents) + if (shouldManageModal) { + // We will wait for feature availability response before handling the modal lifecycle + if (!isFeatureAvailabilityLoading && availability) { + // If the KYC feature is not available we can safely switch to the next modal + if (!isKycAvailable) { + shell.getPlugin('ux').notifyModalActionDone(); + } + // Otherwise we will wait for the KYC procedure status to decide either we display th modal or switch to the + // next one + else if (!isProcedureStatusLoading && statusDataResponse) { + // If the procedure's status is 'required' we display the modal + if (statusDataResponse?.data?.status === requiredStatusKey) { + setShowModal(true); + } + // Otherwise we go to the next modal (if it exists) + else if (shouldManageModal) { + shell.getPlugin('ux').notifyModalActionDone(); + } + } } } - }, [isKycAvailable, current, isProcedureStatusLoading, statusDataResponse]); + }, [current, isFeatureAvailabilityLoading, availability, isKycAvailable, isProcedureStatusLoading, statusDataResponse]); useEffect(() => { if (showModal) { From ad73f5c0618669dbd20015df260a17c57ee37d42 Mon Sep 17 00:00:00 2001 From: CDS Translator Agent Date: Thu, 2 Jan 2025 11:58:14 +0000 Subject: [PATCH 12/22] fix(i18n): add missing translations [CDS 3592] Signed-off-by: CDS Translator Agent --- .../translations/agreements-update-modal/Messages_de_DE.json | 5 +++++ .../translations/agreements-update-modal/Messages_en_GB.json | 5 +++++ .../translations/agreements-update-modal/Messages_es_ES.json | 5 +++++ .../translations/agreements-update-modal/Messages_fr_CA.json | 5 +++++ .../translations/agreements-update-modal/Messages_it_IT.json | 5 +++++ .../translations/agreements-update-modal/Messages_pl_PL.json | 5 +++++ .../translations/agreements-update-modal/Messages_pt_PT.json | 5 +++++ 7 files changed, 35 insertions(+) create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_de_DE.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_en_GB.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_es_ES.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_CA.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_it_IT.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pl_PL.json create mode 100644 packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pt_PT.json diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_de_DE.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_de_DE.json new file mode 100644 index 000000000000..ef127c96d64b --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_de_DE.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "Die Servicebedingungen von OVHcloud ändern sich.", + "agreements_update_modal_description": "Klicken Sie hier, um sich genauer darüber zu informieren.", + "agreements_update_modal_action": "Meine Verträge" +} diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_en_GB.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_en_GB.json new file mode 100644 index 000000000000..46fd10a032a3 --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_en_GB.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "OVHcloud’s Terms and Conditions of Service have been updated.", + "agreements_update_modal_description": "Please click here to read it.", + "agreements_update_modal_action": "My contracts" +} diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_es_ES.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_es_ES.json new file mode 100644 index 000000000000..fb121373fd54 --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_es_ES.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "Le informamos de que hemos actualizado nuestras Condiciones de Servicio.", + "agreements_update_modal_description": "Le invitamos a consultar la versión actualizada aquí.", + "agreements_update_modal_action": "Mis contratos" +} diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_CA.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_CA.json new file mode 100644 index 000000000000..df8c84cbfef3 --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_CA.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/public/translations/agreements-update-modal/Messages_it_IT.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_it_IT.json new file mode 100644 index 000000000000..191d11b7893f --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_it_IT.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "Le Condizioni di Servizio di OVHcloud sono state aggiornate.", + "agreements_update_modal_description": "Ti invitiamo a consultarle cliccando qui.", + "agreements_update_modal_action": "I miei contratti " +} diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pl_PL.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pl_PL.json new file mode 100644 index 000000000000..eacd6ecd9f13 --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pl_PL.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "Warunki świadczenia usług OVHcloud ulegają zmianie.", + "agreements_update_modal_description": "Zaktualizowany dokument znajdziesz tutaj.", + "agreements_update_modal_action": "Regulaminy" +} diff --git a/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pt_PT.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pt_PT.json new file mode 100644 index 000000000000..632be5d54400 --- /dev/null +++ b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_pt_PT.json @@ -0,0 +1,5 @@ +{ + "agreements_update_modal_title": "As condições de serviço da OVHcloud vão sofrer modificações.", + "agreements_update_modal_description": "Sugerimos que as leia clicando aqui.", + "agreements_update_modal_action": "Contratos" +} From ee682fc6bf9ded86e43a4dfb8adb2dd2580f9a9c Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Thu, 2 Jan 2025 15:37:13 +0100 Subject: [PATCH 13/22] feat(billing): backport the new code to billing app ref: MANAGER-14722 Signed-off-by: Maxime Bajeux --- .../src/autoRenew/agreements/user-agreements.controller.js | 7 +++++++ .../modules/new-billing/src/payment/method/add/routing.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/packages/manager/modules/new-billing/src/autoRenew/agreements/user-agreements.controller.js b/packages/manager/modules/new-billing/src/autoRenew/agreements/user-agreements.controller.js index 01343c9a50c9..8562bcdbd49d 100644 --- a/packages/manager/modules/new-billing/src/autoRenew/agreements/user-agreements.controller.js +++ b/packages/manager/modules/new-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/new-billing/src/payment/method/add/routing.js b/packages/manager/modules/new-billing/src/payment/method/add/routing.js index 7df0d7cc3b3d..13d5c759ac7d 100644 --- a/packages/manager/modules/new-billing/src/payment/method/add/routing.js +++ b/packages/manager/modules/new-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 6d0f61cbda036945120e14e690096d34291dd095 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 3 Jan 2025 11:20:54 +0100 Subject: [PATCH 14/22] fix(container): update variable names and dependencies ref: MANAGER-14722 Signed-off-by: Maxime Bajeux --- .../manager/apps/container/src/api/agreements.ts | 4 ++-- .../AgreementsUpdateModal.component.tsx | 6 +++--- .../AgreementsUpdateModal.spec.tsx | 2 +- .../src/hooks/agreements/useAgreementsUpdate.tsx | 12 ------------ .../src/hooks/agreements/usePendingAgreements.tsx | 12 ++++++++++++ 5 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx create mode 100644 packages/manager/apps/container/src/hooks/agreements/usePendingAgreements.tsx diff --git a/packages/manager/apps/container/src/api/agreements.ts b/packages/manager/apps/container/src/api/agreements.ts index 5f5d163d8ca5..59667e85a42a 100644 --- a/packages/manager/apps/container/src/api/agreements.ts +++ b/packages/manager/apps/container/src/api/agreements.ts @@ -1,6 +1,6 @@ import { fetchIcebergV6, FilterComparator } from "@ovh-ux/manager-core-api"; -const fetchAgreementsUpdates = async () => { +const fetchPendingAgreements = async () => { const { data } = await fetchIcebergV6({ route: '/me/agreements', filters: [{ key: 'agreed', comparator: FilterComparator.IsIn, value: ['todo', 'ko'] }], @@ -8,4 +8,4 @@ const fetchAgreementsUpdates = async () => { return data; }; -export default fetchAgreementsUpdates; +export default fetchPendingAgreements; 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 e7d2e7089eab..721250666df1 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -1,12 +1,12 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; -import useAgreementsUpdate from '@/hooks/agreements/useAgreementsUpdate'; +import usePendingAgreements from '@/hooks/agreements/usePendingAgreements'; import { ODS_THEME_COLOR_HUE, ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE } from '@ovhcloud/ods-common-theming'; 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 { 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 { useAuthorizationIam } from '@ovh-ux/manager-react-components'; import useAccountUrn from '@/hooks/accountUrn/useAccountUrn'; import { ModalTypes } from '@/context/modals/modals.context'; import { useModals } from '@/context/modals'; @@ -30,7 +30,7 @@ export default function AgreementsUpdateModal () { const { t } = useTranslation('agreements-update-modal'); 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 { data: agreements, isLoading: areAgreementsLoading } = usePendingAgreements({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { setShowModal(false); navigation.navigateTo('dedicated', `#/billing/autorenew/agreements`); 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 afacb68c6bc3..f7790adcb3e4 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx @@ -72,7 +72,7 @@ vi.mock('@/context/modals', () => ({ useModals: () => ({ current: ModalTypes.agreements }) })); -vi.mock('@/hooks/agreements/useAgreementsUpdate', () => ({ +vi.mock('@/hooks/agreements/usePendingAgreements', () => ({ default: () => ({ data: mocks.agreements }) })); diff --git a/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx b/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx deleted file mode 100644 index 860f6ef7a4d6..000000000000 --- a/packages/manager/apps/container/src/hooks/agreements/useAgreementsUpdate.tsx +++ /dev/null @@ -1,12 +0,0 @@ -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/agreements/usePendingAgreements.tsx b/packages/manager/apps/container/src/hooks/agreements/usePendingAgreements.tsx new file mode 100644 index 000000000000..1c753825b666 --- /dev/null +++ b/packages/manager/apps/container/src/hooks/agreements/usePendingAgreements.tsx @@ -0,0 +1,12 @@ +import { DefinedInitialDataOptions, useQuery } from '@tanstack/react-query'; +import fetchPendingAgreements from '@/api/agreements'; +import { Agreements } from '@/types/agreements'; + +const usePendingAgreements = (options?: Partial>) => + useQuery({ + ...options, + queryKey: ['pending-agreements'], + queryFn: fetchPendingAgreements, + }); + +export default usePendingAgreements; From 1da363a82d8698e9488242bc6a84e90ea247f2c8 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Mon, 6 Jan 2025 14:31:35 +0100 Subject: [PATCH 15/22] feat(container): enhanced readability by splitting code and comments ref: MANAGER-14722 Signed-off-by: Jacques Larique --- packages/manager/apps/container/package.json | 2 +- .../AgreementsUpdateModal.component.tsx | 59 ++++++++++++------- .../IdentityDocumentsModal.tsx | 29 +++++---- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/manager/apps/container/package.json b/packages/manager/apps/container/package.json index 544563eaa1c9..bed21baa470f 100644 --- a/packages/manager/apps/container/package.json +++ b/packages/manager/apps/container/package.json @@ -31,7 +31,7 @@ "@emotion/styled": "^11.10.0", "@ovh-ux/manager-config": "^8.0.2", "@ovh-ux/manager-core-api": "^0.9.0", - "@ovh-ux/manager-react-components": "^1.41.1", + "@ovh-ux/manager-react-components": "^1.43.0", "@ovh-ux/manager-react-shell-client": "^0.8.5", "@ovh-ux/manager-vite-config": "^0.8.3", "@ovh-ux/ovh-payment-method": "^0.5.1", 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 721250666df1..de6bf4046b1b 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -26,9 +26,11 @@ export default function AgreementsUpdateModal () { '#/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 isCurrentModalActive = useMemo(() => current === ModalTypes.agreements, [current]); + const isOnAgreementsPage = useMemo(() => window.location.href === myContractsLink, [window.location.href]); + const isFeatureAvailable = useMemo(() => region !== 'US', [region]); const { t } = useTranslation('agreements-update-modal'); - const { data: urn } = useAccountUrn({ enabled: shouldTryToDisplay }); + const { data: urn } = useAccountUrn({ enabled: isCurrentModalActive && !isOnAgreementsPage && isFeatureAvailable }); const { isAuthorized: canUserAcceptAgreements, isLoading: isAuthorizationLoading } = useAuthorizationIam(['account:apiovh:me/agreements/accept'], urn); const { data: agreements, isLoading: areAgreementsLoading } = usePendingAgreements({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { @@ -36,28 +38,43 @@ export default function AgreementsUpdateModal () { navigation.navigateTo('dedicated', `#/billing/autorenew/agreements`); }; + /* + Since we don't want to display multiple modals at the same time we "watch" the `current` modal, and once it is + the agreements modal turn, we will try to display it (if conditions are met) or switch to the next one otherwise. + As a result, only once the agreements modal is the current one will we manage the modal lifecycle. + Lifecycle management: + - If user is on the agreements page, we will not display the modal and let the page notify for modal change + once the user accept non-validated agreements or leave the page + - Wait until all necessary data (IAM authorization, non-validated agreements list) are loaded + - Once we have the data, check if they allow the display of the modal (IAM authorized + at least one non-validated + agreement), if the conditions are met, we show the modal, otherwise we switch to the next one + */ useEffect(() => { - const shouldManageModal = region !== 'US' && current === ModalTypes.agreements && window.location.href !== myContractsLink; - // We consider we have loaded all information if conditions are not respected to try to display the modal - const hasFullyLoaded = !shouldManageModal - // Or authorization are loaded but user does have right to accept contract or has no contract to accept - || !isAuthorizationLoading && (!canUserAcceptAgreements || !areAgreementsLoading); - // We handle the modal display only when the Agreements modal is the current one, contract management is available - // (region is not US) and we are not on the page where the user can do the related action (accepts his contract) - if (shouldManageModal) { - // We will wait for all data to be retrieved before handling the modal lifecycle - if (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); - } + if (!isCurrentModalActive) return; + + const hasFullyLoaded = !isAuthorizationLoading && (!canUserAcceptAgreements || !areAgreementsLoading); + + if (isFeatureAvailable) { + if (isOnAgreementsPage || !hasFullyLoaded) return; + + if (!agreements?.length) { + shell.getPlugin('ux').notifyModalActionDone(); + } else { + setShowModal(true); } + } else { + shell.getPlugin('ux').notifyModalActionDone(); } - }, [canUserAcceptAgreements, isAuthorizationLoading, canUserAcceptAgreements, areAgreementsLoading, agreements, current]); + return; + }, [ + isCurrentModalActive, + isFeatureAvailable, + isOnAgreementsPage, + isAuthorizationLoading, + canUserAcceptAgreements, + areAgreementsLoading, + agreements, + ]); return showModal ? ( <> 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 1053112ee6b2..12d1969f4715 100644 --- a/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx +++ b/packages/manager/apps/container/src/identity-documents-modal/IdentityDocumentsModal.tsx @@ -66,33 +66,42 @@ export const IdentityDocumentsModal: FunctionComponent = () => { }); navigationPlugin.navigateTo('dedicated', `#/identity-documents`); }; - + /* + Since we don't want to display multiple modals at the same time we "watch" the `current` modal, and once it is + the agreements modal turn, we will try to display it (if conditions are met) or switch to the next one otherwise. + As a result, only once the agreements modal is the current one will we manage the modal lifecycle. + Lifecycle management: + - If user is on the KYC page, we will not display the modal and let the page notify for modal change + once the user has uploaded his documents or leave the page + - Wait until all necessary data (feature flipping, procedure status) are loaded + - Once we have the data, check if they allow the display of the modal (FF authorized + procedure status is + 'required'), if the conditions are met, we show the modal, otherwise we switch to the next one + */ useEffect(() => { const shouldManageModal = current === ModalTypes.kyc && window.location.href !== kycURL; - // We handle the modal display only when the KYC modal is the current one, and we are not on the page - // where the user can do the related action (upload his documents) if (shouldManageModal) { - // We will wait for feature availability response before handling the modal lifecycle if (!isFeatureAvailabilityLoading && availability) { - // If the KYC feature is not available we can safely switch to the next modal if (!isKycAvailable) { shell.getPlugin('ux').notifyModalActionDone(); } - // Otherwise we will wait for the KYC procedure status to decide either we display th modal or switch to the - // next one else if (!isProcedureStatusLoading && statusDataResponse) { - // If the procedure's status is 'required' we display the modal if (statusDataResponse?.data?.status === requiredStatusKey) { setShowModal(true); } - // Otherwise we go to the next modal (if it exists) else if (shouldManageModal) { shell.getPlugin('ux').notifyModalActionDone(); } } } } - }, [current, isFeatureAvailabilityLoading, availability, isKycAvailable, isProcedureStatusLoading, statusDataResponse]); + }, [ + current, + isFeatureAvailabilityLoading, + availability, + isKycAvailable, + isProcedureStatusLoading, + statusDataResponse, + ]); useEffect(() => { if (showModal) { From 4deee7f40fb526135ecb1027097276302385bf02 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Wed, 8 Jan 2025 17:54:37 +0100 Subject: [PATCH 16/22] feat(container): adjusted condition to handle agreement modal lifecycle ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 de6bf4046b1b..e101645b2973 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -11,7 +11,6 @@ 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'; -import { requiredStatusKey } from '@/identity-documents-modal/constants'; export default function AgreementsUpdateModal () { const { shell } = useContext(ApplicationContext); @@ -22,8 +21,8 @@ export default function AgreementsUpdateModal () { const navigation = shell.getPlugin('navigation'); const { current } = useModals(); const myContractsLink = navigation.getURL( - 'dedicated', - '#/billing/autorenew/agreements', + 'billing', + '#/autorenew/agreements', ); const [ showModal, setShowModal ] = useState(false); const isCurrentModalActive = useMemo(() => current === ModalTypes.agreements, [current]); @@ -52,9 +51,11 @@ export default function AgreementsUpdateModal () { useEffect(() => { if (!isCurrentModalActive) return; - const hasFullyLoaded = !isAuthorizationLoading && (!canUserAcceptAgreements || !areAgreementsLoading); - if (isFeatureAvailable) { + const hasFullyLoaded = + !isAuthorizationLoading && + (!canUserAcceptAgreements || + (!areAgreementsLoading && Boolean(agreements))); if (isOnAgreementsPage || !hasFullyLoaded) return; if (!agreements?.length) { From 8f127912f5f807b17c0c0ccab0b78e5859d23f2b Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Thu, 9 Jan 2025 09:26:44 +0100 Subject: [PATCH 17/22] feat(container): handle redirection for dedicated and new-billing ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 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 e101645b2973..b6b4e729d2e3 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -1,27 +1,44 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; -import usePendingAgreements from '@/hooks/agreements/usePendingAgreements'; -import { ODS_THEME_COLOR_HUE, ODS_THEME_COLOR_INTENT, ODS_THEME_TYPOGRAPHY_SIZE } from '@ovhcloud/ods-common-theming'; -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 { Trans, useTranslation } from 'react-i18next'; +import { + ODS_THEME_COLOR_HUE, + ODS_THEME_COLOR_INTENT, + ODS_THEME_TYPOGRAPHY_SIZE, +} from '@ovhcloud/ods-common-theming'; +import { + OsdsButton, + OsdsLink, + OsdsModal, + OsdsText, +} from '@ovhcloud/ods-components/react'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { + ODS_BUTTON_SIZE, + ODS_BUTTON_VARIANT, + ODS_TEXT_LEVEL, +} from '@ovhcloud/ods-components'; +import { useAuthorizationIam } from '@ovh-ux/manager-react-components'; +import usePendingAgreements from '@/hooks/agreements/usePendingAgreements'; import ApplicationContext from '@/context'; import ovhCloudLogo from '@/assets/images/logo-ovhcloud.png'; -import { useAuthorizationIam } from '@ovh-ux/manager-react-components'; 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); - const region: string = shell + const environment = shell .getPlugin('environment') - .getEnvironment() - .getRegion(); + .getEnvironment(); + const region: string = environment.getRegion(); const navigation = shell.getPlugin('navigation'); const { current } = useModals(); + // TODO: simplify this once new-billing is fully open to the public + const billingAppName = environment.getApplicationURL('new-billing') + ? 'new-billing' + : 'dedicated'; const myContractsLink = navigation.getURL( - 'billing', + billingAppName, '#/autorenew/agreements', ); const [ showModal, setShowModal ] = useState(false); @@ -34,7 +51,7 @@ export default function AgreementsUpdateModal () { const { data: agreements, isLoading: areAgreementsLoading } = usePendingAgreements({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { setShowModal(false); - navigation.navigateTo('dedicated', `#/billing/autorenew/agreements`); + navigation.navigateTo(billingAppName, `#/billing/autorenew/agreements`); }; /* From 49000d34f28f68dd7c4c14dcb9ae2fd1c5e5fd2c Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Thu, 9 Jan 2025 12:24:42 +0100 Subject: [PATCH 18/22] feat(container): adjusted agreements modal lifecycle management ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal/AgreementsUpdateModal.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 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 b6b4e729d2e3..2b1722885acc 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -70,9 +70,9 @@ export default function AgreementsUpdateModal () { if (isFeatureAvailable) { const hasFullyLoaded = + urn && !isAuthorizationLoading && - (!canUserAcceptAgreements || - (!areAgreementsLoading && Boolean(agreements))); + (!canUserAcceptAgreements || !areAgreementsLoading); if (isOnAgreementsPage || !hasFullyLoaded) return; if (!agreements?.length) { From b2d62939f305793b50c671663e63a4314c36baa0 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Thu, 9 Jan 2025 14:15:12 +0100 Subject: [PATCH 19/22] feat(container): adjusted mocks for agreements modal tests ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 f7790adcb3e4..ed54f3f0b55d 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx @@ -32,6 +32,7 @@ const shellContext = { case 'environment': return { getEnvironment: () => ({ getRegion: vi.fn(() => mocks.region), + getApplicationURL: vi.fn((app) => `https://ovh.com/manager/${app}`) }) }; } @@ -61,11 +62,11 @@ vi.mock('react', async (importOriginal) => { }); vi.mock('@/hooks/accountUrn/useAccountUrn', () => ({ - default: () => () => 'urn' + default: () => ({ data: 'urn' }), })); -vi.mock('@ovh-ux/manager-react-components/src/hooks/iam', () => ({ - useAuthorizationIam: () => () => ({ isAuthorized: mocks.isAuthorized }) +vi.mock('@ovh-ux/manager-react-components', () => ({ + useAuthorizationIam: () => ({ isAuthorized: mocks.isAuthorized }) })); vi.mock('@/context/modals', () => ({ From 4a3efbf6eba38e2a8780e700d741186eb548e945 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 10 Jan 2025 09:55:41 +0100 Subject: [PATCH 20/22] feat(account): backport account code from dedicated ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../user-identity-documents.controller.js | 23 ++++++++++++++++++- .../user-identity-documents.html | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/manager/modules/account/src/identity-documents/user-identity-documents.controller.js b/packages/manager/modules/account/src/identity-documents/user-identity-documents.controller.js index 7a9023315141..9075dde12a68 100644 --- a/packages/manager/modules/account/src/identity-documents/user-identity-documents.controller.js +++ b/packages/manager/modules/account/src/identity-documents/user-identity-documents.controller.js @@ -25,7 +25,15 @@ 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.$q = $q; this.$http = $http; this.$scope = $scope; @@ -62,6 +70,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) { @@ -127,6 +138,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/modules/account/src/identity-documents/user-identity-documents.html b/packages/manager/modules/account/src/identity-documents/user-identity-documents.html index 033ed66d44ce..3747d715917f 100644 --- a/packages/manager/modules/account/src/identity-documents/user-identity-documents.html +++ b/packages/manager/modules/account/src/identity-documents/user-identity-documents.html @@ -160,6 +160,6 @@ From 1e082ad37069da7c0c5054899fc38be0ecf140b8 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 10 Jan 2025 10:59:36 +0100 Subject: [PATCH 21/22] feat(container): fixed agreements page links ref: MANAGER-14722 Signed-off-by: Jacques Larique --- .../AgreementsUpdateModal.component.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 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 2b1722885acc..da8d6adbdd45 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -34,13 +34,10 @@ export default function AgreementsUpdateModal () { const navigation = shell.getPlugin('navigation'); const { current } = useModals(); // TODO: simplify this once new-billing is fully open to the public - const billingAppName = environment.getApplicationURL('new-billing') - ? 'new-billing' - : 'dedicated'; - const myContractsLink = navigation.getURL( - billingAppName, - '#/autorenew/agreements', - ); + const isNewBillingAvailable = Boolean(environment.getApplicationURL('new-billing')); + const billingAppName = isNewBillingAvailable ? 'new-billing' : 'dedicated'; + const billingAppPath = `#${isNewBillingAvailable ? '' : 'billing/'}/autorenew/agreements`; + const myContractsLink = navigation.getURL(billingAppName, billingAppPath); const [ showModal, setShowModal ] = useState(false); const isCurrentModalActive = useMemo(() => current === ModalTypes.agreements, [current]); const isOnAgreementsPage = useMemo(() => window.location.href === myContractsLink, [window.location.href]); @@ -51,7 +48,7 @@ export default function AgreementsUpdateModal () { const { data: agreements, isLoading: areAgreementsLoading } = usePendingAgreements({ enabled: canUserAcceptAgreements }); const goToContractPage = () => { setShowModal(false); - navigation.navigateTo(billingAppName, `#/billing/autorenew/agreements`); + navigation.navigateTo(billingAppName, billingAppPath); }; /* From d6acbae92f8bba28f51f12e3c8e57d3d89e081f1 Mon Sep 17 00:00:00 2001 From: Jacques Larique Date: Fri, 10 Jan 2025 11:37:39 +0100 Subject: [PATCH 22/22] feat(container): fixed agreement 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 da8d6adbdd45..3531a840f10c 100644 --- a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -36,7 +36,7 @@ export default function AgreementsUpdateModal () { // TODO: simplify this once new-billing is fully open to the public const isNewBillingAvailable = Boolean(environment.getApplicationURL('new-billing')); const billingAppName = isNewBillingAvailable ? 'new-billing' : 'dedicated'; - const billingAppPath = `#${isNewBillingAvailable ? '' : 'billing/'}/autorenew/agreements`; + const billingAppPath = `#/${isNewBillingAvailable ? '' : 'billing/'}autorenew/agreements`; const myContractsLink = navigation.getURL(billingAppName, billingAppPath); const [ showModal, setShowModal ] = useState(false); const isCurrentModalActive = useMemo(() => current === ModalTypes.agreements, [current]);