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/api/agreements.ts b/packages/manager/apps/container/src/api/agreements.ts new file mode 100644 index 000000000000..59667e85a42a --- /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 fetchPendingAgreements = async () => { + const { data } = await fetchIcebergV6({ + route: '/me/agreements', + filters: [{ key: 'agreed', comparator: FilterComparator.IsIn, value: ['todo', 'ko'] }], + }); + return data; +}; + +export default fetchPendingAgreements; 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..d3d11208fdb0 --- /dev/null +++ b/packages/manager/apps/container/src/api/authorizations.ts @@ -0,0 +1,18 @@ +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', + }); + + return data[0]?.urn; +}; 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 new file mode 100644 index 000000000000..3531a840f10c --- /dev/null +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.component.tsx @@ -0,0 +1,151 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +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 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); + const environment = shell + .getPlugin('environment') + .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 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]); + const isFeatureAvailable = useMemo(() => region !== 'US', [region]); + const { t } = useTranslation('agreements-update-modal'); + 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 = () => { + setShowModal(false); + navigation.navigateTo(billingAppName, billingAppPath); + }; + + /* + 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(() => { + if (!isCurrentModalActive) return; + + if (isFeatureAvailable) { + const hasFullyLoaded = + urn && + !isAuthorizationLoading && + (!canUserAcceptAgreements || !areAgreementsLoading); + if (isOnAgreementsPage || !hasFullyLoaded) return; + + if (!agreements?.length) { + shell.getPlugin('ux').notifyModalActionDone(); + } else { + setShowModal(true); + } + } else { + shell.getPlugin('ux').notifyModalActionDone(); + } + return; + }, [ + isCurrentModalActive, + isFeatureAvailable, + isOnAgreementsPage, + isAuthorizationLoading, + canUserAcceptAgreements, + areAgreementsLoading, + agreements, + ]); + + return showModal ? ( + <> + +
+ ovh-cloud-logo +
+ + {t('agreements_update_modal_title')} + + +

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

+
+ + + {t('agreements_update_modal_action')} + +
+ + ) : null; +} diff --git a/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx new file mode 100644 index 000000000000..ed54f3f0b55d --- /dev/null +++ b/packages/manager/apps/container/src/components/AgreementsUpdateModal/AgreementsUpdateModal.spec.tsx @@ -0,0 +1,100 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import { vi } from 'vitest'; +import { + ShellContext, + 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, + region: 'US', + agreements: [], +})); + +const shellContext = { + shell: { + getPlugin: (plugin: string) => { + switch (plugin) { + case 'navigation': return { + getURL: vi.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve('http://fakelink.com'), 50); + }), + ), + }; + case 'ux': return { + notifyModalActionDone: vi.fn(), + }; + case 'environment': return { + getEnvironment: () => ({ + getRegion: vi.fn(() => mocks.region), + getApplicationURL: vi.fn((app) => `https://ovh.com/manager/${app}`) + }) + }; + } + }, + } +}; + +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: () => ({ data: 'urn' }), +})); + +vi.mock('@ovh-ux/manager-react-components', () => ({ + useAuthorizationIam: () => ({ isAuthorized: mocks.isAuthorized }) +})); + +vi.mock('@/context/modals', () => ({ + useModals: () => ({ current: ModalTypes.agreements }) +})); + +vi.mock('@/hooks/agreements/usePendingAgreements', () => ({ + 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 nothing 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/container/index.tsx b/packages/manager/apps/container/src/container/index.tsx index e56c26a88084..2ac8148d14e3 100644 --- a/packages/manager/apps/container/src/container/index.tsx +++ b/packages/manager/apps/container/src/container/index.tsx @@ -12,6 +12,9 @@ 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 '@/context/modals/useModals'; +import { ModalsProvider } from '@/context/modals'; export default function Container(): JSX.Element { const { @@ -81,16 +84,19 @@ export default function Container(): JSX.Element { - {isCookiePolicyApplied && - - - - } - {isCookiePolicyApplied && - - - - } + {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 d044ad74870b..fa03a4c90384 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'; @@ -64,7 +64,7 @@ const CookiePolicy = ({ shell, onValidate }: Props): JSX.Element => { setCookies('MANAGER_TRACKING', agreed ? 1 : 0); trackingPlugin.onUserConsentFromModal(agreed); setShow(false); - onValidate(true); + onValidate(); } useEffect(() => { 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/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; 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..12d1969f4715 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 { data: statusDataResponse } = useIdentityDocumentsStatus({ - enabled: Boolean(availability && availability[kycIndiaFeature] && !storage), + const isKycAvailable = useMemo(() => Boolean(availability && availability[kycIndiaFeature] && !storage), [availability, storage]); + + const { data: statusDataResponse, isLoading: isProcedureStatusLoading } = useIdentityDocumentsStatus({ + enabled: isKycAvailable && current === ModalTypes.kyc && window.location.href !== kycURL, }); const trackingPlugin = shell.getPlugin('tracking'); @@ -51,13 +52,13 @@ export const IdentityDocumentsModal: FunctionComponent = () => { setStorage(true); trackingPlugin.trackClick({ name: `${trackingPrefix}::pop-up::link::kyc::cancel`, + type: 'action', ...trackingContext, }); }; const onConfirm = () => { setShowModal(false); - setStorage(true); trackingPlugin.trackClick({ name: `${trackingPrefix}::pop-up::button::kyc::start-verification`, type: 'action', @@ -65,12 +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(() => { - if (statusDataResponse?.data?.status === requiredStatusKey) { - setShowModal(true); + const shouldManageModal = current === ModalTypes.kyc && window.location.href !== kycURL; + if (shouldManageModal) { + if (!isFeatureAvailabilityLoading && availability) { + if (!isKycAvailable) { + shell.getPlugin('ux').notifyModalActionDone(); + } + else if (!isProcedureStatusLoading && statusDataResponse) { + if (statusDataResponse?.data?.status === requiredStatusKey) { + setShowModal(true); + } + else if (shouldManageModal) { + shell.getPlugin('ux').notifyModalActionDone(); + } + } + } } - }, [statusDataResponse?.data?.status]); + }, [ + current, + isFeatureAvailabilityLoading, + availability, + isKycAvailable, + isProcedureStatusLoading, + statusDataResponse, + ]); useEffect(() => { if (showModal) { diff --git a/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx b/packages/manager/apps/container/src/payment-modal/PaymentModal.tsx index 107be967efaf..45f4ab98f4e3 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; @@ -33,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) { @@ -57,31 +59,41 @@ const PaymentModal = (): JSX.Element => { 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 { data: paymentResponse } = useQuery({ + const isReadyToRequest = current === ModalTypes.payment && window.location.href !== paymentMethodURL; + + const { data: paymentResponse, isLoading } = useQuery({ queryKey: ['me-payment-method'], - queryFn: () => fetchIcebergV6({ route: '/me/payment/method' }) + queryFn: () => fetchIcebergV6({ route: '/me/payment/method' }), + enabled: isReadyToRequest, }); useEffect(() => { - if (paymentResponse) { - const alert = computeAlert(paymentResponse.data); + if (isReadyToRequest && !isLoading) { + const alert = computeAlert(paymentResponse?.data); if (alert) { setAlert(alert); setShowPaymentModal(true); } + else { + shell.getPlugin('ux').notifyModalActionDone(); + } } - }, [paymentResponse]); + }, [paymentResponse, isReadyToRequest, isLoading]); return !showPaymentModal ? ( <> 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_fr_FR.json b/packages/manager/apps/container/src/public/translations/agreements-update-modal/Messages_fr_FR.json new file mode 100644 index 000000000000..df8c84cbfef3 --- /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/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" +} 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, +})); 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; +}; 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/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 @@ 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( { 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( {