diff --git a/.github/actions/download_remote_config_backup/action.yml b/.github/actions/download_remote_config_backup/action.yml index 1d4efadb3794..e636dfba135c 100644 --- a/.github/actions/download_remote_config_backup/action.yml +++ b/.github/actions/download_remote_config_backup/action.yml @@ -3,12 +3,6 @@ description: Download remote config backup inputs: REMOTE_CONFIG_URL: description: "Remote config backup URL" - GPG_PRIVATE_KEY: - description: "GPG private key" - GPG_PASSPHRASE: - description: "GPG passphrase" - GITHUB_TOKEN: - description: "GitHub token" runs: using: composite steps: diff --git a/.github/workflows/release_test.yml b/.github/workflows/release_test.yml index c3ee063e6ebb..941427f59e82 100644 --- a/.github/workflows/release_test.yml +++ b/.github/workflows/release_test.yml @@ -16,13 +16,6 @@ jobs: uses: "./.github/actions/setup_node" - name: Install Dependencies uses: "./.github/actions/npm_install_from_cache" - - name: Download Remote Config Backup File - uses: ./.github/actions/download_remote_config_backup - with: - GITHUB_TOKEN: ${{ github.token }} - REMOTE_CONFIG_URL: ${{ vars.REMOTE_CONFIG_URL }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Build uses: "./.github/actions/build" with: diff --git a/.github/workflows/remote_config_pr.yml b/.github/workflows/remote_config_pr.yml index b0fe2e200695..ea80fc71c888 100644 --- a/.github/workflows/remote_config_pr.yml +++ b/.github/workflows/remote_config_pr.yml @@ -1,4 +1,12 @@ name: Deriv App Remote Config backup file + +permissions: + actions: write + checks: write + contents: write + deployments: write + pull-requests: write + statuses: write on: push: branches: @@ -13,24 +21,24 @@ jobs: steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - name: Import GPG key + id: import-gpg + uses: deriv-com/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true - name: Checkout to remote_config_update_branch run: | git fetch origin git checkout "${{ env.REMOTE_CONFIG_BRANCH }}" || git checkout -b "${{ env.REMOTE_CONFIG_BRANCH }}" origin/"${{ env.REMOTE_CONFIG_BRANCH }}" - + shell: bash - name: Download Remote Config Backup File uses: ./.github/actions/download_remote_config_backup with: REMOTE_CONFIG_URL: ${{ vars.REMOTE_CONFIG_URL }} - - name: Import GPG key - id: import-gpg - uses: deriv-com/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - name: Commit the file and Create PR env: GIT_AUTHOR_NAME: ${{ steps.import-gpg.outputs.name }} diff --git a/package-lock.json b/package-lock.json index a84926629817..728a67570ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@datadog/browser-logs": "^4.36.0", "@datadog/browser-rum": "^4.37.0", "@deriv-com/analytics": "1.4.10", - "@deriv-com/ui": "1.5.3", + "@deriv-com/ui": "1.8.1", "@deriv/api-types": "^1.0.118", "@deriv/deriv-api": "^1.0.15", "@deriv/deriv-charts": "^2.0.5", @@ -2937,9 +2937,9 @@ } }, "node_modules/@deriv-com/ui": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.5.3.tgz", - "integrity": "sha512-DbW13EqHeY4aekueXX6AaiYzKKu7jSFt9z7ofB/OsE42T0MkrYRszRV/TjP3INXJDUwaUVZIvuF5ffWzSFeCEw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.8.1.tgz", + "integrity": "sha512-JIkPV42IPItan1MDhQtjgx8AfIgR2bZKc38k8bDQxuEpQATHa9xwfoh6jsh/LArNsJBiXLWKb33AJE/54YOO8Q==", "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.9.6" } @@ -52383,9 +52383,9 @@ } }, "@deriv-com/ui": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.5.3.tgz", - "integrity": "sha512-DbW13EqHeY4aekueXX6AaiYzKKu7jSFt9z7ofB/OsE42T0MkrYRszRV/TjP3INXJDUwaUVZIvuF5ffWzSFeCEw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@deriv-com/ui/-/ui-1.8.1.tgz", + "integrity": "sha512-JIkPV42IPItan1MDhQtjgx8AfIgR2bZKc38k8bDQxuEpQATHa9xwfoh6jsh/LArNsJBiXLWKb33AJE/54YOO8Q==", "requires": { "@rollup/rollup-linux-x64-gnu": "^4.9.6" } diff --git a/packages/account-v2/package.json b/packages/account-v2/package.json index 7e4a11057ed3..aec2f5c56755 100644 --- a/packages/account-v2/package.json +++ b/packages/account-v2/package.json @@ -11,7 +11,7 @@ "start": "rimraf dist && npm run test && npm run serve" }, "dependencies": { - "@deriv-com/ui": "1.5.3", + "@deriv-com/ui": "1.8.1", "@deriv/api": "^1.0.0", "@deriv/library": "^1.0.0", "@deriv/quill-design": "^1.3.2", diff --git a/packages/account-v2/src/containers/ManualForm/__tests__/manual-form.spec.tsx b/packages/account-v2/src/containers/ManualForm/__tests__/manual-form.spec.tsx index b0ed75e7e37a..7f54077de497 100644 --- a/packages/account-v2/src/containers/ManualForm/__tests__/manual-form.spec.tsx +++ b/packages/account-v2/src/containers/ManualForm/__tests__/manual-form.spec.tsx @@ -16,6 +16,8 @@ describe('ManualForm', () => { selectedDocument?: React.ComponentProps['selectedDocument']; }) => { const mockProps: React.ComponentProps = { + formData: {}, + isExpiryDateRequired: false, onCancel: jest.fn(), onSubmit: jest.fn(), selectedDocument: selectedDocument ?? 'driving_licence', diff --git a/packages/account-v2/src/containers/ManualForm/manual-form.tsx b/packages/account-v2/src/containers/ManualForm/manual-form.tsx index d91bbe3c936c..83563c6d0b6b 100644 --- a/packages/account-v2/src/containers/ManualForm/manual-form.tsx +++ b/packages/account-v2/src/containers/ManualForm/manual-form.tsx @@ -3,7 +3,6 @@ import { Form, Formik, FormikValues } from 'formik'; import { InferType } from 'yup'; import { Button } from '@deriv-com/ui'; import { MANUAL_DOCUMENT_SELFIE, TManualDocumentTypes } from '../../constants/manualFormConstants'; -import { useManualForm } from '../../hooks'; import { getManualFormValidationSchema, setInitialValues } from '../../utils/manual-form-utils'; import { ManualFormDocumentUpload } from './manual-form-document-upload'; import { ManualFormFooter } from './manual-form-footer'; @@ -13,14 +12,19 @@ type TmanualDocumentFormValues = InferType void; onSubmit: (values: TmanualDocumentFormValues) => void; selectedDocument: TManualDocumentTypes; }; -export const ManualForm = ({ formData, onCancel, onSubmit, selectedDocument }: TManualFormProps) => { - const { isExpiryDateRequired } = useManualForm(); - +export const ManualForm = ({ + formData, + isExpiryDateRequired, + onCancel, + onSubmit, + selectedDocument, +}: TManualFormProps) => { const validationSchema = getManualFormValidationSchema(selectedDocument, isExpiryDateRequired); const initialValues = useMemo(() => { @@ -40,7 +44,10 @@ export const ManualForm = ({ formData, onCancel, onSubmit, selectedDocument }: T
- +
diff --git a/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts b/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts index 69413e7ecc31..e5f94c949bef 100644 --- a/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts +++ b/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts @@ -1,46 +1,55 @@ -import { useSettings } from '@deriv/api'; +import { useKycAuthStatus } from '@deriv/api'; import { renderHook } from '@testing-library/react-hooks'; import useManualForm from '../useManualForm'; jest.mock('@deriv/api', () => ({ ...jest.requireActual('@deriv/api'), - useSettings: jest.fn(), + useKycAuthStatus: jest.fn(), })); describe('useManualForm', () => { - it('should return false if citizenship is ng', () => { - (useSettings as jest.Mock).mockReturnValue({ - data: { - citizen: 'ng', - country_code: 'in', + it('should return false for expiry date if citizenship is ng and document is nimc slip', () => { + (useKycAuthStatus as jest.Mock).mockReturnValue({ + isLoading: false, + kyc_auth_status: { + identity: { + available_services: ['onfido', 'manual'], + }, }, }); - const { result } = renderHook(() => useManualForm()); - const { isExpiryDateRequired } = result.current; - expect(isExpiryDateRequired).toBe(false); + const { result } = renderHook(() => useManualForm('ng', 'nimc_slip')); + const { isExpiryDateRequired, poiService } = result.current; + expect(isExpiryDateRequired).toBeFalsy(); + expect(poiService).toBe('manual'); }); - it('should return true if citizenship and country code is not ng', () => { - (useSettings as jest.Mock).mockReturnValue({ - data: { - citizen: 'au', - country_code: 'au', + it('should return true for expiry date if citizenship is ng and document is passport', () => { + (useKycAuthStatus as jest.Mock).mockReturnValue({ + isLoading: false, + kyc_auth_status: { + identity: { + available_services: ['onfido', 'manual'], + }, }, }); - const { result } = renderHook(() => useManualForm()); - const { isExpiryDateRequired } = result.current; - expect(isExpiryDateRequired).toBe(true); + const { result } = renderHook(() => useManualForm('ng', 'passport')); + const { isExpiryDateRequired, poiService } = result.current; + expect(isExpiryDateRequired).toBeTruthy(); + expect(poiService).toBe('onfido'); }); - it('should return true if citizenship is not ng but country code is ng', () => { - (useSettings as jest.Mock).mockReturnValue({ - data: { - citizen: 'in', - country_code: 'ng', + it('should return manual as POI service if citizenship is in and document is passport', () => { + (useKycAuthStatus as jest.Mock).mockReturnValue({ + isLoading: false, + kyc_auth_status: { + identity: { + available_services: ['onfido', 'manual'], + }, }, }); - const { result } = renderHook(() => useManualForm()); - const { isExpiryDateRequired } = result.current; - expect(isExpiryDateRequired).toBe(true); + const { result } = renderHook(() => useManualForm('in', 'passport')); + const { isExpiryDateRequired, poiService } = result.current; + expect(isExpiryDateRequired).toBeTruthy(); + expect(poiService).toBe('manual'); }); }); diff --git a/packages/account-v2/src/hooks/useManualForm.ts b/packages/account-v2/src/hooks/useManualForm.ts index dcdba4bc310b..541bf6e1be1b 100644 --- a/packages/account-v2/src/hooks/useManualForm.ts +++ b/packages/account-v2/src/hooks/useManualForm.ts @@ -1,13 +1,32 @@ -import { useSettings } from '@deriv/api'; +import { useKycAuthStatus } from '@deriv/api'; +import { MANUAL_DOCUMENT_TYPES, TManualDocumentTypes } from '../constants/manualFormConstants'; /** A custom hook used for manual verification flow */ -const useManualForm = () => { - const { data: settings } = useSettings(); - const countryCode = settings?.citizen ?? settings?.country_code; - const isExpiryDateRequired = !!countryCode && countryCode !== 'ng'; +const useManualForm = (countryCode: string, selectedDocument: TManualDocumentTypes) => { + const { isLoading, kyc_auth_status, ...rest } = useKycAuthStatus({ country: countryCode }); + const servicesAvailable = kyc_auth_status?.identity?.available_services; + if (countryCode === 'ng') { + if (selectedDocument === MANUAL_DOCUMENT_TYPES.NIMC_SLIP) { + return { + isExpiryDateRequired: false, + isLoading, + poiService: 'manual', + ...rest, + }; + } + return { + isExpiryDateRequired: true, + isLoading, + poiService: isLoading ? null : servicesAvailable[0], + ...rest, + }; + } return { - isExpiryDateRequired, + isExpiryDateRequired: true, + isLoading, + poiService: 'manual', + ...rest, }; }; diff --git a/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx b/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx index e3f027becfcc..53a971d727c1 100644 --- a/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx +++ b/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx @@ -1,7 +1,6 @@ import React, { ComponentProps } from 'react'; import { Formik } from 'formik'; -import { fireEvent, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; import { IDVForm } from '../idv-form'; jest.mock('@deriv-com/ui', () => ({ diff --git a/packages/account-v2/src/modules/Onfido/OnfidoContainer.tsx b/packages/account-v2/src/modules/Onfido/OnfidoContainer.tsx index bbaf80f7c021..a1f320fb7f5b 100644 --- a/packages/account-v2/src/modules/Onfido/OnfidoContainer.tsx +++ b/packages/account-v2/src/modules/Onfido/OnfidoContainer.tsx @@ -7,6 +7,7 @@ import { Button, Loader, Text } from '@deriv-com/ui'; import IcAccountMissingDetails from '../../assets/proof-of-identity/ic-account-missing-details.svg'; import { ErrorMessage } from '../../components/ErrorMessage'; import { IconWithMessage } from '../../components/IconWithMessage'; +import { TManualDocumentTypes } from '../../constants/manualFormConstants'; import { OnfidoView } from '../../containers/Onfido/OnfidoView'; // TODO: Remove optional and default props when POI is ready @@ -14,9 +15,15 @@ type TOnfidoContainer = { country?: string; isEnabledByDefault?: boolean; onOnfidoSubmit?: () => void; + selectedDocument?: TManualDocumentTypes; }; -export const OnfidoContainer = ({ country = 'co', isEnabledByDefault = false, onOnfidoSubmit }: TOnfidoContainer) => { +export const OnfidoContainer = ({ + country, + isEnabledByDefault = false, + onOnfidoSubmit, + selectedDocument, +}: TOnfidoContainer) => { const [isOnfidoEnabled, setIsOnfidoEnabled] = useState(isEnabledByDefault); const [transitionEnd, setTransitionEnd] = useState(false); const history = useHistory(); @@ -27,7 +34,7 @@ export const OnfidoContainer = ({ country = 'co', isEnabledByDefault = false, on isServiceTokenLoading, onfidoInitializationError, serviceTokenError, - } = useOnfido(country); + } = useOnfido(country, selectedDocument); useEffect(() => { if (hasSubmitted) { diff --git a/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx b/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx index 64f9ff60fc70..11e140c7098c 100644 --- a/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx +++ b/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx @@ -1,10 +1,12 @@ /* eslint-disable no-console */ import React, { useState } from 'react'; import { InferType } from 'yup'; +import { Loader } from '@deriv-com/ui'; import { TManualDocumentTypes } from '../../constants/manualFormConstants'; import { ManualForm } from '../../containers/ManualForm'; import { SelfieDocumentUpload } from '../../containers/SelfieDocumentUpload'; import { useManualForm } from '../../hooks'; +import { OnfidoContainer } from '../../modules/Onfido'; import { getManualFormValidationSchema, getSelfieValidationSchema } from '../../utils/manual-form-utils'; type TManualUploadContainerProps = { @@ -13,7 +15,10 @@ type TManualUploadContainerProps = { }; export const ManualUploadContainer = ({ selectedDocument, setSelectedDocument }: TManualUploadContainerProps) => { - const { isExpiryDateRequired } = useManualForm(); + const { isExpiryDateRequired, isLoading, poiService } = useManualForm( + 'ng', + selectedDocument as TManualDocumentTypes + ); const manualUpload = getManualFormValidationSchema( selectedDocument as TManualDocumentTypes, @@ -25,7 +30,9 @@ export const ManualUploadContainer = ({ selectedDocument, setSelectedDocument }: const [formData, setFormData] = useState>({}); const [shouldUploadSelfie, setShouldUploadSelfie] = useState(false); - console.log('formData: ', formData); + if (isLoading || poiService === null) { + return ; + } if (shouldUploadSelfie) { return ( @@ -38,17 +45,31 @@ export const ManualUploadContainer = ({ selectedDocument, setSelectedDocument }: /> ); } + + if (poiService === 'manual') { + return ( + { + console.log('Called on Cancel'); + setSelectedDocument(null); + }} + onSubmit={values => { + console.log('Called submit'); + setFormData(prev => ({ ...prev, ...values })); + setShouldUploadSelfie(true); + }} + selectedDocument={selectedDocument as TManualDocumentTypes} + /> + ); + } return ( - { - console.log('Called on Cancel'); - setSelectedDocument(null); - }} - onSubmit={values => { - console.log('Called submit'); - setFormData(prev => ({ ...prev, ...values })); - setShouldUploadSelfie(true); + { + console.log('Called Onfido submit'); }} selectedDocument={selectedDocument as TManualDocumentTypes} /> diff --git a/packages/account-v2/src/router/constants/routesConfig.tsx b/packages/account-v2/src/router/constants/routesConfig.tsx index 6a5e7aa0cbbd..437904c2c8a9 100644 --- a/packages/account-v2/src/router/constants/routesConfig.tsx +++ b/packages/account-v2/src/router/constants/routesConfig.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ACCOUNT_V2_ROUTES } from '../../constants/routes'; +import { ManualUpload } from '../../modules'; import { POAFormContainer } from '../../modules/POAForm/POAFormContainer'; -import { ManualUploadContainer } from '../../pages/ManualFormContainer/manual-form-container'; import DummyRoute from '../components/dummy-route/dummy-route'; export const routes = [ @@ -27,12 +27,7 @@ export const routes = [ }, { // TODO: Replace this with POI container - routeComponent: () => ( - console.log('Called with', val)} - /> - ), + routeComponent: () => , routeName: 'Proof of identity', routePath: ACCOUNT_V2_ROUTES.ProofOfIdentity, }, diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index ba9c884ced8a..f7f1c0418b09 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -84,7 +84,8 @@ const PersonalDetails = observer( trackEvent({ action: 'close', }); - }, [trackEvent]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); //is_rendered_for_idv is used for configuring the components when they are used in idv page const is_rendered_for_idv = shouldShowIdentityInformation({ diff --git a/packages/api/src/hooks/useKycAuthStatus.ts b/packages/api/src/hooks/useKycAuthStatus.ts index 9c6f1a80cdb6..20f0e660013d 100644 --- a/packages/api/src/hooks/useKycAuthStatus.ts +++ b/packages/api/src/hooks/useKycAuthStatus.ts @@ -1,14 +1,16 @@ import { TSocketRequestPayload } from '../../types'; import useQuery from '../useQuery'; +import useAuthorize from './useAuthorize'; type TKycAuthStatusPayload = TSocketRequestPayload<'kyc_auth_status'>['payload']; /** Custom hook that returns Proof of Identity (POI) and Proof of Address (POA) authentication status details. */ const useKycAuthStatus = (payload: TKycAuthStatusPayload) => { + const { isSuccess } = useAuthorize(); const { data, ...kyc_auth_status_rest } = useQuery('kyc_auth_status', { payload, + options: { enabled: isSuccess }, }); - return { /** The KYC auth status */ kyc_auth_status: data?.kyc_auth_status, diff --git a/packages/api/src/hooks/useOnfido.ts b/packages/api/src/hooks/useOnfido.ts index c1110e10a785..a09a62c6b1bf 100644 --- a/packages/api/src/hooks/useOnfido.ts +++ b/packages/api/src/hooks/useOnfido.ts @@ -11,6 +11,7 @@ import { v4 as uuidv4 } from 'uuid'; * To initialize Onfido, ensure that an empty container is present. * Call the hook and use `onfidoContainerId` to mark the empty container where the Onfido UI is to be mounted. * @param [country] - The country code to be used to retrieve the Onfido service token. + * @param [selectedDocument] - Type of document to be passed to bypass the document selection screen * For example: * ``` * const { data: { onfidoContainerId } } = useOnfido() @@ -24,7 +25,7 @@ import { v4 as uuidv4 } from 'uuid'; * ) * ``` */ -const useOnfido = (country?: string) => { +const useOnfido = (country?: string, selectedDocument?: string) => { // use to check that we do not re-attempt to reload the onfido script while its still loading const [isOnfidoLoading, setIsOnfidoLoading] = useState(false); const [isOnfidoInitialized, setIsOnfidoInitialized] = useState(false); @@ -72,7 +73,9 @@ const useOnfido = (country?: string) => { if (countryCode && residenceList.length) { const onfidoResidence = residenceList.find(residence => residence?.value === countryCode)?.identity ?.services?.onfido; - + if (selectedDocument && onfidoResidence?.documents_supported) { + return [onfidoResidence?.documents_supported[selectedDocument]?.display_name]; + } if (onfidoResidence && onfidoResidence.is_country_supported) { return Object.keys(onfidoResidence.documents_supported ?? {}).map( (document: string) => onfidoResidence.documents_supported?.[document].display_name @@ -80,7 +83,7 @@ const useOnfido = (country?: string) => { } } return []; - }, [residenceList, countryCode]); + }, [residenceList, countryCode, selectedDocument]); const onComplete = useCallback( (data: Omit & { data?: { id?: string } }) => { diff --git a/packages/api/src/hooks/useRemoteConfig.ts b/packages/api/src/hooks/useRemoteConfig.ts index c059093aa38b..066878a580fa 100644 --- a/packages/api/src/hooks/useRemoteConfig.ts +++ b/packages/api/src/hooks/useRemoteConfig.ts @@ -2,10 +2,12 @@ import { useQuery } from '@tanstack/react-query'; import initData from '../remote_config.json'; const remoteConfigQuery = async function () { - if (!process.env.REMOTE_CONFIG_URL) { + const isProductionOrStaging = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging'; + const REMOTE_CONFIG_URL = process.env.REMOTE_CONFIG_URL || ''; + if (isProductionOrStaging && REMOTE_CONFIG_URL === '') { throw new Error('Remote Config URL is not set!'); } - const response = await fetch(process.env.REMOTE_CONFIG_URL); + const response = await fetch(REMOTE_CONFIG_URL); if (!response.ok) { throw new Error('Remote Config Server is out of reach!'); } diff --git a/packages/api/src/remote_config.json b/packages/api/src/remote_config.json index 6e5686f610a6..ec86c6867301 100644 --- a/packages/api/src/remote_config.json +++ b/packages/api/src/remote_config.json @@ -1,8 +1 @@ -{ - "cs_chat_livechat": true, - "cs_chat_whatsapp": true, - "tracking_datadog": true, - "marketing_growthbook": false, - "tracking_hotjar": true, - "tracking_GTM": true -} +{"cs_chat_livechat":true,"cs_chat_whatsapp":true,"marketing_growthbook":false,"tracking_GTM":true,"tracking_datadog":true,"tracking_hotjar":true} \ No newline at end of file diff --git a/packages/appstore/src/components/cfds-listing/cfds-listing.scss b/packages/appstore/src/components/cfds-listing/cfds-listing.scss index f5ac6be923aa..6028cc5e9a88 100644 --- a/packages/appstore/src/components/cfds-listing/cfds-listing.scss +++ b/packages/appstore/src/components/cfds-listing/cfds-listing.scss @@ -447,6 +447,10 @@ display: flex; align-items: center; gap: 0.8rem; + + .dc-btn:hover { + text-decoration: underline; + } } &-action { text-decoration: underline; diff --git a/packages/cashier-v2/package.json b/packages/cashier-v2/package.json index fe49287bf309..fa254465ecfe 100644 --- a/packages/cashier-v2/package.json +++ b/packages/cashier-v2/package.json @@ -12,7 +12,7 @@ "start": "rimraf dist && npm run test && npm run serve" }, "dependencies": { - "@deriv-com/ui": "1.5.3", + "@deriv-com/ui": "1.8.1", "@deriv/api": "^1.0.0", "@deriv/integration": "^1.0.0", "@deriv/library": "^1.0.0", diff --git a/packages/cashier-v2/src/assets/.gitkeep b/packages/cashier-v2/src/assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/cashier-v2/src/components/CashierLayout/CashierLayout.tsx b/packages/cashier-v2/src/components/CashierLayout/CashierLayout.tsx deleted file mode 100644 index e01dfda7167c..000000000000 --- a/packages/cashier-v2/src/components/CashierLayout/CashierLayout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { useDevice } from '@deriv-com/ui'; -import type { TSideNotes } from '../../types'; -import styles from './CashierLayout.module.scss'; - -type TProps = { - children: React.ReactNode; - sideNotes: TSideNotes; -}; - -const CashierLayout: React.FC = ({ children, sideNotes }) => { - const { isMobile } = useDevice(); - const isTopMobileSideNotesVisible = isMobile && sideNotes.position === 'top' && sideNotes.notes.length > 0; - const isBottomMobileSideNotesVisible = isMobile && sideNotes.position === 'bottom' && sideNotes.notes.length > 0; - - return ( -
-
- {isTopMobileSideNotesVisible &&
{sideNotes.notes}
} - {children} - {isBottomMobileSideNotesVisible &&
{sideNotes.notes}
} -
- {!isMobile &&
{sideNotes.notes}
} -
- ); -}; - -export default CashierLayout; diff --git a/packages/cashier-v2/src/components/CashierLayout/index.ts b/packages/cashier-v2/src/components/CashierLayout/index.ts deleted file mode 100644 index ad62a4cad19c..000000000000 --- a/packages/cashier-v2/src/components/CashierLayout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CashierLayout } from './CashierLayout'; diff --git a/packages/cashier-v2/src/components/NewsTicker/NewsTicker.module.scss b/packages/cashier-v2/src/components/NewsTicker/NewsTicker.module.scss new file mode 100644 index 000000000000..9e02736c9df2 --- /dev/null +++ b/packages/cashier-v2/src/components/NewsTicker/NewsTicker.module.scss @@ -0,0 +1,38 @@ +@keyframes news-ticker { + 0% { + transform: translate3d(100%, 0, 0); + } + 100% { + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes news-ticker-2 { + 0% { + transform: translate3d(0, 0, 0); + } + 100% { + transform: translate3d(-200%, 0, 0); + } +} + +.container { + display: flex; + white-space: nowrap; + overflow: hidden; +} + +.children { + animation-iteration-count: infinite; + animation-name: news-ticker; + animation-timing-function: linear; + will-change: transform; + + &:nth-child(2) { + animation-name: news-ticker-2; + } +} + +.item { + display: inline-block; +} diff --git a/packages/cashier-v2/src/components/NewsTicker/NewsTicker.tsx b/packages/cashier-v2/src/components/NewsTicker/NewsTicker.tsx new file mode 100644 index 000000000000..129f8a73a711 --- /dev/null +++ b/packages/cashier-v2/src/components/NewsTicker/NewsTicker.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import clsx from 'clsx'; +import NewsTickerChildren from './NewsTickerChildren'; +import styles from './NewsTicker.module.scss'; + +type TNewsTicker = { + children: React.ReactNode; + className?: string; + speed: number; +}; + +const NewsTicker: React.FC = ({ children, className, speed }) => { + const [elementWidth, setElementWidth] = useState(-1); + const [isExceedingParent, setIsExceedingParent] = useState(false); + + const onRefChange = (ref: HTMLDivElement) => { + if (ref) { + setIsExceedingParent(ref.scrollWidth > ref.clientWidth); + + if (elementWidth === -1) { + setElementWidth(ref.scrollWidth); + } + } + }; + + const animationDuration = elementWidth / speed; // time = distance / speed + + return ( +
+ + {isExceedingParent && ( + + )} +
+ ); +}; + +export default NewsTicker; diff --git a/packages/cashier-v2/src/components/NewsTicker/NewsTickerChildren.tsx b/packages/cashier-v2/src/components/NewsTicker/NewsTickerChildren.tsx new file mode 100644 index 000000000000..c2373f5ddc35 --- /dev/null +++ b/packages/cashier-v2/src/components/NewsTicker/NewsTickerChildren.tsx @@ -0,0 +1,36 @@ +import React, { Children } from 'react'; +import styles from './NewsTicker.module.scss'; + +type TNewsTickerChildren = { + animationDuration: number; + isExceedingParent?: boolean; + isSecondContainer?: boolean; + reactChildren: React.ReactNode; +}; + +const NewsTickerChildren = ({ + animationDuration, + isExceedingParent, + isSecondContainer, + reactChildren: children, +}: TNewsTickerChildren) => ( +
+ {Children.map(children, (child, idx) => ( +
+ {child} +
+ ))} +
+); + +export default NewsTickerChildren; diff --git a/packages/cashier-v2/src/components/NewsTicker/index.ts b/packages/cashier-v2/src/components/NewsTicker/index.ts new file mode 100644 index 000000000000..d85d9fd011c7 --- /dev/null +++ b/packages/cashier-v2/src/components/NewsTicker/index.ts @@ -0,0 +1 @@ +export { default as NewsTicker } from './NewsTicker'; diff --git a/packages/cashier-v2/src/components/CashierLayout/CashierLayout.module.scss b/packages/cashier-v2/src/components/PageContainer/PageContainer.module.scss similarity index 100% rename from packages/cashier-v2/src/components/CashierLayout/CashierLayout.module.scss rename to packages/cashier-v2/src/components/PageContainer/PageContainer.module.scss diff --git a/packages/cashier-v2/src/components/PageContainer/PageContainer.tsx b/packages/cashier-v2/src/components/PageContainer/PageContainer.tsx new file mode 100644 index 000000000000..3e64419096fb --- /dev/null +++ b/packages/cashier-v2/src/components/PageContainer/PageContainer.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useDevice } from '@deriv-com/ui'; +import styles from './PageContainer.module.scss'; + +type TSidebar = { + content: JSX.Element; + contentPosition?: 'bottom' | 'top'; +}; + +type TProps = { + children: React.ReactNode; + rightSidebar?: TSidebar; +}; + +const PageContainer: React.FC = ({ children, rightSidebar }) => { + const { isMobile } = useDevice(); + const isTopMobileContentVisible = isMobile && rightSidebar?.contentPosition === 'top' && rightSidebar.content; + const isBottomMobileContentVisible = isMobile && rightSidebar?.contentPosition === 'bottom' && rightSidebar.content; + + return ( +
+
+ {isTopMobileContentVisible &&
{rightSidebar.content}
} + {children} + {isBottomMobileContentVisible &&
{rightSidebar.content}
} +
+ {!isMobile &&
{rightSidebar?.content}
} +
+ ); +}; + +export default PageContainer; diff --git a/packages/cashier-v2/src/components/PageContainer/index.ts b/packages/cashier-v2/src/components/PageContainer/index.ts new file mode 100644 index 000000000000..fe6238f753a5 --- /dev/null +++ b/packages/cashier-v2/src/components/PageContainer/index.ts @@ -0,0 +1 @@ +export { default as PageContainer } from './PageContainer'; diff --git a/packages/cashier-v2/src/components/index.ts b/packages/cashier-v2/src/components/index.ts index 7492e50e49f9..dfb81c7ec872 100644 --- a/packages/cashier-v2/src/components/index.ts +++ b/packages/cashier-v2/src/components/index.ts @@ -1,2 +1,3 @@ -export { CashierLayout } from './CashierLayout'; export { DummyComponent } from './DummyComponent'; +export { NewsTicker } from './NewsTicker'; +export { PageContainer } from './PageContainer'; diff --git a/packages/cashier-v2/src/containers/Cashier/Cashier.tsx b/packages/cashier-v2/src/containers/Cashier/Cashier.tsx index 0d29b0a10d62..19fc091aac42 100644 --- a/packages/cashier-v2/src/containers/Cashier/Cashier.tsx +++ b/packages/cashier-v2/src/containers/Cashier/Cashier.tsx @@ -1,16 +1,11 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Route, Switch, useHistory } from 'react-router-dom'; import { useAuthorize } from '@deriv/api'; import { Loader, PageLayout, VerticalTab, VerticalTabItems } from '@deriv-com/ui'; -import { CashierLayout } from '../../components'; -import type { TRouteTypes, TSideNotes } from '../../types'; +import type { TRouteTypes } from '../../types'; import styles from './Cashier.module.scss'; const Cashier: React.FC = ({ routes }) => { - const [sideNotes, setSideNotes] = useState({ - notes: [], - position: undefined, - }); const routesWithID = routes?.map(route => ({ ...route, id: route.path })) ?? []; const { isLoading } = useAuthorize(); const history = useHistory(); @@ -35,18 +30,16 @@ const Cashier: React.FC = ({ routes }) => { } > - - - {routes?.map(route => { - const { path, title } = route; - return ( - - - - ); - })} - - + + {routes?.map(route => { + const { path, title } = route; + return ( + + + + ); + })} +
); diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.module.scss b/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.module.scss new file mode 100644 index 000000000000..84b8bc9e5ce2 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 2.4rem; +} diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.tsx new file mode 100644 index 000000000000..e9d71cb82853 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/CashierOnboarding.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { CashierOnboardingAccountIdentifierMessage } from './components/CashierOnboardingAccountIdentifierMessage'; +import { CashierOnboardingCryptoCard } from './components/CashierOnboardingCryptoCard'; +import { CashierOnboardingFiatCard } from './components/CashierOnboardingFiatCard'; +import { CashierOnboardingOnrampCard } from './components/CashierOnboardingOnrampCard'; +import { CashierOnboardingP2PCard } from './components/CashierOnboardingP2pCard'; +import { CashierOnboardingPaymentAgentCard } from './components/CashierOnboardingPaymentAgentCard'; +import styles from './CashierOnboarding.module.scss'; + +const CashierOnboarding: React.FC = () => { + return ( +
+ + + + + + +
+ ); +}; + +export default CashierOnboarding; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/CashierOnboardingAccountIdentifierMessage.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/CashierOnboardingAccountIdentifierMessage.tsx new file mode 100644 index 000000000000..8b411b66cb6b --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/CashierOnboardingAccountIdentifierMessage.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useActiveAccount } from '@deriv/api'; +import { InlineMessage, Text } from '@deriv-com/ui'; + +const CashierOnboardingAccountIdentifierMessage: React.FC = () => { + const { data: activeAccount } = useActiveAccount(); + + const message = `This is your ${activeAccount?.currency_config?.display_code} account ${activeAccount?.loginid}.`; + + return ( + + {message} + + ); +}; + +export default CashierOnboardingAccountIdentifierMessage; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/__test__/CashierOnboardingAccountIdentifierMessage.spec.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/__test__/CashierOnboardingAccountIdentifierMessage.spec.tsx new file mode 100644 index 000000000000..4e6914c20a16 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/__test__/CashierOnboardingAccountIdentifierMessage.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import CashierOnboardingAccountIdentifierMessage from '../CashierOnboardingAccountIdentifierMessage'; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useActiveAccount: jest.fn(() => ({ + data: { + currency_config: { + display_code: 'USD', + }, + loginid: 'CR1234567', + }, + })), +})); + +describe('CashierOnboardingAccountIdentifierMessage', () => { + test('should show proper identifier message', () => { + render(); + + expect(screen.getByText('This is your USD account CR1234567.')).toBeInTheDocument(); + }); +}); diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/index.ts new file mode 100644 index 000000000000..80ec41de4e0e --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingAccountIdentifierMessage/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingAccountIdentifierMessage } from './CashierOnboardingAccountIdentifierMessage'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.module.scss b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.module.scss new file mode 100644 index 000000000000..2e33afe73294 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.module.scss @@ -0,0 +1,27 @@ +.container { + display: flex; + flex-direction: column; + width: 100%; + padding: 1.6rem; + border: 2px solid var(--border-normal-1); + border-radius: 8px; + margin-top: 0.8rem; + cursor: pointer; + gap: 1.6rem; + overflow: hidden; + transition: all 0.25s ease; + + &:hover { + transform: scale(0.99); + } +} + +.content { + display: flex; + align-items: center; + gap: 1.2rem; +} + +.description { + flex: 1; +} diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.tsx new file mode 100644 index 000000000000..dba464be6943 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/CashierOnboardingCard.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { LabelPairedChevronRightMdBoldIcon } from '@deriv/quill-icons'; +import { Text } from '@deriv-com/ui'; +import styles from './CashierOnboardingCard.module.scss'; + +type TProps = { + description: string; + onClick?: VoidFunction; + title: string; +}; + +const CashierOnboardingCard: React.FC> = ({ + children, + description, + onClick, + title, +}) => { + return ( +
+ + {title} + + +
+ ); +}; + +export default CashierOnboardingCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/__test__/CashierOnboardingCard.spec.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/__test__/CashierOnboardingCard.spec.tsx new file mode 100644 index 000000000000..42362c8f159d --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/__test__/CashierOnboardingCard.spec.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CashierOnboardingCard from '../CashierOnboardingCard'; + +describe('CashierOnboardingCard', () => { + test('should call the onClick callback when clicked', () => { + const props: React.ComponentProps = { + description: 'bar', + onClick: jest.fn(), + title: 'foo', + }; + + render(); + + const container = screen.getByTestId('dt_cashier_onboarding_card'); + + userEvent.click(container); + + expect(props.onClick).toBeCalledTimes(1); + }); +}); diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/index.ts new file mode 100644 index 000000000000..5f348850985e --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingCard } from './CashierOnboardingCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/CashierOnboardingCryptoCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/CashierOnboardingCryptoCard.tsx new file mode 100644 index 000000000000..3f483e8304e8 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/CashierOnboardingCryptoCard.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { onboardingCryptoCardIcons } from '../../constants/icons'; +import { CashierOnboardingCard } from '../CashierOnboardingCard'; +import { CashierOnboardingIconMarquee } from '../CashierOnboardingIconMarquee'; + +const CashierOnboardingCryptoCard: React.FC = () => { + return ( + + + + ); +}; + +export default CashierOnboardingCryptoCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/__test__/CashierOnboardingCryptoCard.test.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/__test__/CashierOnboardingCryptoCard.test.tsx new file mode 100644 index 000000000000..4de443bed040 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/__test__/CashierOnboardingCryptoCard.test.tsx @@ -0,0 +1,3 @@ +it('TestPlug', () => Promise.resolve()); + +export {}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/index.ts new file mode 100644 index 000000000000..e354686e1297 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingCryptoCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingCryptoCard } from './CashierOnboardingCryptoCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/CashierOnboardingFiatCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/CashierOnboardingFiatCard.tsx new file mode 100644 index 000000000000..66851e20a11d --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/CashierOnboardingFiatCard.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { onboardingFiatCardIcons } from '../../constants/icons'; +import { CashierOnboardingCard } from '../CashierOnboardingCard'; +import { CashierOnboardingIconMarquee } from '../CashierOnboardingIconMarquee'; + +const CashierOnboardingFiatCard: React.FC = () => { + return ( + + + + ); +}; + +export default CashierOnboardingFiatCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/__test__/CashierOnboardingFiatCard.spec.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/__test__/CashierOnboardingFiatCard.spec.tsx new file mode 100644 index 000000000000..4de443bed040 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/__test__/CashierOnboardingFiatCard.spec.tsx @@ -0,0 +1,3 @@ +it('TestPlug', () => Promise.resolve()); + +export {}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/index.ts new file mode 100644 index 000000000000..4395700cb62c --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingFiatCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingFiatCard } from './CashierOnboardingFiatCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.module.scss b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.module.scss new file mode 100644 index 000000000000..18587c4f07b2 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.module.scss @@ -0,0 +1,8 @@ +.cashier-onboarding-icon-marquee { + overflow: visible; +} + +.container { + display: flex; + gap: 0.8rem; +} diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.tsx new file mode 100644 index 000000000000..4688b4d77161 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/CashierOnboardingIconMarquee.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { NewsTicker } from '../../../../components'; +import { TIcons } from '../../types'; +import styles from './CashierOnboardingIconMarquee.module.scss'; + +type TProps = { + icons: TIcons[]; +}; + +const CashierOnboardingIconMarquee: React.FC = ({ icons }) => { + return ( +
+ +
+ {icons.map(({ icon: Icon, key }) => ( + + ))} +
+
+
+ ); +}; + +export default CashierOnboardingIconMarquee; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/__test__/CashierOnboardingIconMarquee.test.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/__test__/CashierOnboardingIconMarquee.test.tsx new file mode 100644 index 000000000000..8da35ef51ed7 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/__test__/CashierOnboardingIconMarquee.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import CashierOnboardingIconMarquee from '../CashierOnboardingIconMarquee'; + +const FakeIcon = (() =>
FakeIcon
) as unknown as React.ComponentProps< + typeof CashierOnboardingIconMarquee +>['icons'][number]['icon']; + +describe('CashierOnboardingIconMarquee', () => { + test('should render cashier onboarding icon marquee with icons', () => { + const props: React.ComponentProps = { + icons: [ + { icon: FakeIcon, key: '1' }, + { icon: FakeIcon, key: '2' }, + { icon: FakeIcon, key: '3' }, + { icon: FakeIcon, key: '4' }, + ], + }; + + render(); + + const container = screen.getByTestId('dt_cashier_onboarding_icon-marquee'); + + expect(container).toBeInTheDocument(); + expect(screen.getAllByText('FakeIcon')).toHaveLength(4); + }); +}); diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/index.ts new file mode 100644 index 000000000000..053def1eee0f --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingIconMarquee/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingIconMarquee } from './CashierOnboardingIconMarquee'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/CashierOnboardingOnrampCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/CashierOnboardingOnrampCard.tsx new file mode 100644 index 000000000000..651cec53a960 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/CashierOnboardingOnrampCard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useActiveAccount } from '@deriv/api'; +import { onboardingOnrampIcons } from '../../constants/icons'; +import { CashierOnboardingCard } from '../CashierOnboardingCard'; +import { CashierOnboardingIconMarquee } from '../CashierOnboardingIconMarquee'; + +const CashierOnboardingOnrampCard: React.FC = () => { + const { data: activeAccount } = useActiveAccount(); + + return ( + + + + ); +}; + +export default CashierOnboardingOnrampCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/__test__/CashierOnboardingOnrampCard.test.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/__test__/CashierOnboardingOnrampCard.test.tsx new file mode 100644 index 000000000000..4de443bed040 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/__test__/CashierOnboardingOnrampCard.test.tsx @@ -0,0 +1,3 @@ +it('TestPlug', () => Promise.resolve()); + +export {}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/index.ts new file mode 100644 index 000000000000..d0154bf9c6af --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingOnrampCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingOnrampCard } from './CashierOnboardingOnrampCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/CashierOnboardingP2pCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/CashierOnboardingP2pCard.tsx new file mode 100644 index 000000000000..e0a0f7d27e44 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/CashierOnboardingP2pCard.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { CashierOnboardingCard } from '../CashierOnboardingCard'; + +const CashierOnboardingP2PCard: React.FC = () => { + return ( + + ); +}; + +export default CashierOnboardingP2PCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/__test__/CashierOnboardingP2pCard.test.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/__test__/CashierOnboardingP2pCard.test.tsx new file mode 100644 index 000000000000..4de443bed040 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/__test__/CashierOnboardingP2pCard.test.tsx @@ -0,0 +1,3 @@ +it('TestPlug', () => Promise.resolve()); + +export {}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/index.ts new file mode 100644 index 000000000000..b76dcac17d2e --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingP2pCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingP2PCard } from './CashierOnboardingP2pCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/CashierOnboardingPaymentAgentCard.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/CashierOnboardingPaymentAgentCard.tsx new file mode 100644 index 000000000000..b11284722d2e --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/CashierOnboardingPaymentAgentCard.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { CashierOnboardingCard } from '../CashierOnboardingCard'; + +const CashierOnboardingPaymentAgentCard: React.FC = () => { + return ( + + ); +}; + +export default CashierOnboardingPaymentAgentCard; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/__test__/CashierOnboardingPaymentAgentCard.test.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/__test__/CashierOnboardingPaymentAgentCard.test.tsx new file mode 100644 index 000000000000..4de443bed040 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/__test__/CashierOnboardingPaymentAgentCard.test.tsx @@ -0,0 +1,3 @@ +it('TestPlug', () => Promise.resolve()); + +export {}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/index.ts b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/index.ts new file mode 100644 index 000000000000..cff9610b9f75 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/components/CashierOnboardingPaymentAgentCard/index.ts @@ -0,0 +1 @@ +export { default as CashierOnboardingPaymentAgentCard } from './CashierOnboardingPaymentAgentCard'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/constants/icons.ts b/packages/cashier-v2/src/lib/CashierOnboarding/constants/icons.ts new file mode 100644 index 000000000000..761d5e5fad60 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/constants/icons.ts @@ -0,0 +1,60 @@ +import { + PaymentMethodBanxaBrandDarkIcon, + PaymentMethodBanxaBrandIcon, + PaymentMethodBitcoinBrandDarkIcon, + PaymentMethodBitcoinBrandIcon, + PaymentMethodCreditDebitBrandDarkIcon, + PaymentMethodCreditDebitBrandIcon, + PaymentMethodEthereumBlackIcon, + PaymentMethodEthereumBrandIcon, + PaymentMethodEWalletBrandDarkIcon, + PaymentMethodEWalletBrandIcon, + PaymentMethodInstantBankTransferBrandDarkIcon, + PaymentMethodInstantBankTransferBrandIcon, + PaymentMethodLitecoinBrandIcon, + PaymentMethodLitecoinWhiteIcon, + PaymentMethodLocalPaymentMethodsBrandDarkIcon, + PaymentMethodLocalPaymentMethodsBrandIcon, + PaymentMethodTetherUsdtBrandIcon, + PaymentMethodTetherUsdtWhiteIcon, + PaymentMethodUsdCoinBrandDarkIcon, + PaymentMethodUsdCoinBrandIcon, +} from '@deriv/quill-icons'; +import type { TOnboardingIconsType } from '../types'; + +export const onboardingFiatCardIcons: TOnboardingIconsType = { + dark: [ + { icon: PaymentMethodCreditDebitBrandDarkIcon, key: 'PaymentMethodCreditDebitBrandDarkIcon' }, + { icon: PaymentMethodInstantBankTransferBrandDarkIcon, key: 'PaymentMethodInstantBankTransferBrandDarkIcon' }, + { icon: PaymentMethodEWalletBrandDarkIcon, key: 'PaymentMethodEWalletBrandDarkIcon' }, + { icon: PaymentMethodLocalPaymentMethodsBrandDarkIcon, key: 'PaymentMethodLocalPaymentMethodsBrandDarkIcon' }, + ], + light: [ + { icon: PaymentMethodCreditDebitBrandIcon, key: 'PaymentMethodCreditDebitBrandIcon' }, + { icon: PaymentMethodInstantBankTransferBrandIcon, key: 'PaymentMethodInstantBankTransferBrandIcon' }, + { icon: PaymentMethodEWalletBrandIcon, key: 'PaymentMethodEWalletBrandIcon' }, + { icon: PaymentMethodLocalPaymentMethodsBrandIcon, key: 'PaymentMethodLocalPaymentMethodsBrandIcon' }, + ], +}; + +export const onboardingCryptoCardIcons: TOnboardingIconsType = { + dark: [ + { icon: PaymentMethodBitcoinBrandDarkIcon, key: 'PaymentMethodBitcoinBrandDarkIcon' }, + { icon: PaymentMethodEthereumBrandIcon, key: 'PaymentMethodEthereumBrandIcon' }, + { icon: PaymentMethodLitecoinWhiteIcon, key: 'PaymentMethodLitecoinWhiteIcon' }, + { icon: PaymentMethodUsdCoinBrandDarkIcon, key: 'PaymentMethodUsdCoinBrandDarkIcon' }, + { icon: PaymentMethodTetherUsdtWhiteIcon, key: 'PaymentMethodTetherUsdtWhiteIcon' }, + ], + light: [ + { icon: PaymentMethodBitcoinBrandIcon, key: 'PaymentMethodBitcoinBrandIcon' }, + { icon: PaymentMethodEthereumBlackIcon, key: 'PaymentMethodEthereumBlackIcon' }, + { icon: PaymentMethodLitecoinBrandIcon, key: 'PaymentMethodLitecoinBrandIcon' }, + { icon: PaymentMethodUsdCoinBrandIcon, key: 'PaymentMethodUsdCoinBrandIcon' }, + { icon: PaymentMethodTetherUsdtBrandIcon, key: 'PaymentMethodTetherUsdtBrandIcon' }, + ], +}; + +export const onboardingOnrampIcons: TOnboardingIconsType = { + dark: [{ icon: PaymentMethodBanxaBrandDarkIcon, key: 'PaymentMethodBanxaBrandDarkIcon' }], + light: [{ icon: PaymentMethodBanxaBrandIcon, key: 'PaymentMethodBanxaBrandIcon' }], +}; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/index.tsx b/packages/cashier-v2/src/lib/CashierOnboarding/index.tsx new file mode 100644 index 000000000000..b39f191d2be4 --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/index.tsx @@ -0,0 +1 @@ +export { default as CashierOnboardingModule } from './CashierOnboarding'; diff --git a/packages/cashier-v2/src/lib/CashierOnboarding/types.ts b/packages/cashier-v2/src/lib/CashierOnboarding/types.ts new file mode 100644 index 000000000000..2ffbffb8d6ea --- /dev/null +++ b/packages/cashier-v2/src/lib/CashierOnboarding/types.ts @@ -0,0 +1,8 @@ +import { IconTypes } from '@deriv/quill-icons'; + +export type TIcons = { icon: IconTypes; key: string }; + +export type TOnboardingIconsType = { + dark: TIcons[]; + light: TIcons[]; +}; diff --git a/packages/cashier-v2/src/lib/index.ts b/packages/cashier-v2/src/lib/index.ts index 116c8dfc0bfa..209563c74638 100644 --- a/packages/cashier-v2/src/lib/index.ts +++ b/packages/cashier-v2/src/lib/index.ts @@ -1,2 +1,3 @@ +export * from './CashierOnboarding'; export * from './DepositFiat'; export * from './WithdrawalVerification'; diff --git a/packages/cashier-v2/src/routes/Router.tsx b/packages/cashier-v2/src/routes/Router.tsx index e02a4b7d8fcc..56c297bac71c 100644 --- a/packages/cashier-v2/src/routes/Router.tsx +++ b/packages/cashier-v2/src/routes/Router.tsx @@ -1,9 +1,9 @@ /* eslint-disable sort-keys */ import React from 'react'; import { Switch } from 'react-router-dom'; -import { DummyComponent } from '../components'; +import { DummyComponent, PageContainer } from '../components'; import { Cashier } from '../containers'; -import { DepositFiatModule, WithdrawalVerificationModule } from '../lib'; +import { CashierOnboardingModule, WithdrawalVerificationModule } from '../lib'; import { TRouteTypes } from '../types'; import RouteWithSubRoutes from './RouteWithSubRoutes'; @@ -25,12 +25,20 @@ const routesConfig: TRouteTypes.IRouteConfig[] = [ routes: [ { path: cashierPathRoutes.cashierDeposit, - component: DepositFiatModule, + component: () => ( + + + + ), title: 'Deposit', }, { path: cashierPathRoutes.cashierWithdrawal, - component: WithdrawalVerificationModule, + component: () => ( + + + + ), title: 'Withdrawal', }, { diff --git a/packages/cashier-v2/src/types.ts b/packages/cashier-v2/src/types.ts index decd1b97cdb7..545c42593772 100644 --- a/packages/cashier-v2/src/types.ts +++ b/packages/cashier-v2/src/types.ts @@ -4,9 +4,7 @@ import { cashierPathRoutes } from './routes/Router'; export namespace TRouteTypes { export type TRoutes = typeof cashierPathRoutes[keyof typeof cashierPathRoutes]; export interface IRouteConfig { - component: React.ComponentType< - Omit & { setSideNotes?: React.Dispatch> } - >; + component: React.ComponentType>; path: string; routes?: IRouteConfig[]; title: string; @@ -24,11 +22,6 @@ export namespace TErrorTypes { }; } -export type TSideNotes = { - notes: JSX.Element[] | []; - position?: 'bottom' | 'top'; -}; - declare module 'react-router-dom' { export function useHistory(): { location: { diff --git a/packages/components/src/components/auto-height-wrapper/auto-height-wrapper.tsx b/packages/components/src/components/auto-height-wrapper/auto-height-wrapper.tsx index dd2b802a158c..89e788569ff9 100644 --- a/packages/components/src/components/auto-height-wrapper/auto-height-wrapper.tsx +++ b/packages/components/src/components/auto-height-wrapper/auto-height-wrapper.tsx @@ -15,17 +15,22 @@ type TAutoHeightWrapperProps = { const AutoHeightWrapper = (props: TAutoHeightWrapperProps) => { const [height, setHeight] = React.useState(props.default_height); const child_client_height_ref = React.useRef(0); + const is_mounted_ref = React.useRef(false); const prev_child_client_height = usePrevious(child_client_height_ref.current); React.useEffect(() => { + is_mounted_ref.current = true; window.addEventListener('resize', updateHeight); - return () => window.removeEventListener('resize', updateHeight); + return () => { + is_mounted_ref.current = false; + window.removeEventListener('resize', updateHeight); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const updateHeight = () => { - if (props.default_height) { + if (props.default_height && is_mounted_ref.current) { setHeight( child_client_height_ref.current > props.default_height ? child_client_height_ref.current - (props.height_offset || 0) diff --git a/packages/components/src/components/dropdown/dropdown.tsx b/packages/components/src/components/dropdown/dropdown.tsx index 5b5cae940398..d653e21d2a13 100644 --- a/packages/components/src/components/dropdown/dropdown.tsx +++ b/packages/components/src/components/dropdown/dropdown.tsx @@ -198,7 +198,7 @@ const DropdownList = React.forwardRef((props, lis ref={list_ref} > 200 ? list_dimensions[1] : '200px'} scroll_height={scroll_height} should_scroll_to_selected={should_scroll_to_selected} autohide={should_autohide} diff --git a/packages/core/src/Stores/client-store.js b/packages/core/src/Stores/client-store.js index ee19950b77cf..4dfda10650a9 100644 --- a/packages/core/src/Stores/client-store.js +++ b/packages/core/src/Stores/client-store.js @@ -2321,7 +2321,9 @@ export default class ClientStore extends BaseStore { }) .finally(() => { setTimeout(() => { - const { event, analyticsData } = window.dataLayer.find(el => el.event === 'ce_questionnaire_form'); + const { event, analyticsData } = window.dataLayer.find( + el => el.event === 'ce_questionnaire_form' + ) ?? { event: 'unhandled', analyticsData: {} }; Analytics.trackEvent(event, analyticsData); }, 10000); }); diff --git a/packages/p2p-v2/package.json b/packages/p2p-v2/package.json index d98596d3a80c..b72ef3c3c063 100644 --- a/packages/p2p-v2/package.json +++ b/packages/p2p-v2/package.json @@ -12,7 +12,7 @@ "start": "rimraf dist && npm run test && npm run serve" }, "dependencies": { - "@deriv-com/ui": "1.5.3", + "@deriv-com/ui": "1.8.1", "@deriv/api": "^1.0.0", "@deriv/integration": "^1.0.0", "@deriv/quill-icons": "^1.18.3", diff --git a/packages/p2p-v2/src/components/Modals/BlockUnblockUserModal/__tests__/BlockUnblockUserModal.spec.tsx b/packages/p2p-v2/src/components/Modals/BlockUnblockUserModal/__tests__/BlockUnblockUserModal.spec.tsx new file mode 100644 index 000000000000..5bb7d4dbebbd --- /dev/null +++ b/packages/p2p-v2/src/components/Modals/BlockUnblockUserModal/__tests__/BlockUnblockUserModal.spec.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { APIProvider } from '@deriv/api'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import BlockUnblockUserModal from '../BlockUnblockUserModal'; + +const wrapper = ({ children }: { children: JSX.Element }) => ( + +
+ {children} + +); + +const mockOnRequestClose = jest.fn(); +const mockUseBlockMutate = jest.fn(); +const mockUseUnblockMutate = jest.fn(); + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + p2p: { + counterparty: { + useBlock: jest.fn(() => ({ + mutate: mockUseBlockMutate, + })), + useUnblock: jest.fn(() => ({ + mutate: mockUseUnblockMutate, + })), + }, + }, +})); + +describe('BlockUnblockUserModal', () => { + it('should render the modal with correct title and behaviour for blocking user', () => { + render( + , + { + wrapper, + } + ); + + expect( + screen.queryByText( + `You won't see Jane Doe's ads anymore and they won't be able to place orders on your ads.` + ) + ).toBeVisible(); + + const blockBtn = screen.getByRole('button', { + name: 'Block', + }); + userEvent.click(blockBtn); + + expect(mockUseBlockMutate).toBeCalledWith([1]); + }); + it('should render the modal with correct title and behaviour for unblocking user', () => { + render( + , + { + wrapper, + } + ); + + expect( + screen.queryByText( + `You will be able to see Hu Tao's ads. They'll be able to place orders on your ads, too.` + ) + ).toBeVisible(); + + const unblockBtn = screen.getByRole('button', { + name: 'Unblock', + }); + userEvent.click(unblockBtn); + + expect(mockUseUnblockMutate).toBeCalledWith([2]); + }); + it('should hide the modal when user clicks cancel', () => { + render( + , + { + wrapper, + } + ); + + const cancelBtn = screen.getByRole('button', { + name: 'Cancel', + }); + userEvent.click(cancelBtn); + + expect(mockOnRequestClose).toBeCalled(); + }); +}); diff --git a/packages/p2p-v2/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx b/packages/p2p-v2/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx new file mode 100644 index 000000000000..af7ade69b28a --- /dev/null +++ b/packages/p2p-v2/src/components/Modals/DailyLimitModal/__tests__/DailyLimitModal.spec.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { APIProvider } from '@deriv/api'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import DailyLimitModal from '../DailyLimitModal'; + +const wrapper = ({ children }: { children: JSX.Element }) => ( + +
+ {children} + +); + +const mockUseAdvertiserUpdateMutate = jest.fn(); +const mockOnRequestClose = jest.fn(); +let mockUseAdvertiserUpdate = { + data: { + daily_buy_limit: 100, + daily_sell_limit: 200, + }, + error: undefined, + isLoading: true, + isSuccess: false, + mutate: mockUseAdvertiserUpdateMutate, +}; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + p2p: { + advertiser: { + useUpdate: jest.fn(() => mockUseAdvertiserUpdate), + }, + }, +})); + +describe('DailyLimitModal', () => { + it('should render loader when data is not ready', () => { + render(, { wrapper }); + + expect(screen.getByTestId('dt_derivs-loader')).toBeVisible(); + }); + it('should render the correct title and behaviour', () => { + mockUseAdvertiserUpdate = { + ...mockUseAdvertiserUpdate, + isLoading: false, + isSuccess: false, + }; + render(, { wrapper }); + + expect( + screen.getByText( + `You won’t be able to change your buy and sell limits again after this. Do you want to continue?` + ) + ).toBeVisible(); + + const continueBtn = screen.getByRole('button', { + name: 'Yes, continue', + }); + userEvent.click(continueBtn); + expect(mockUseAdvertiserUpdateMutate).toBeCalledWith({ + upgrade_limits: 1, + }); + }); + it('should render the successful limits increase', () => { + mockUseAdvertiserUpdate = { + ...mockUseAdvertiserUpdate, + isLoading: false, + isSuccess: true, + }; + render(, { wrapper }); + + expect( + screen.getByText(`Your daily limits have been increased to 100 USD (buy) and 200 USD (sell).`) + ).toBeVisible(); + + const okBtn = screen.getByRole('button', { + name: 'Ok', + }); + userEvent.click(okBtn); + expect(mockOnRequestClose).toBeCalled(); + }); + it('should render the error information when limits are unable to be upgraded', () => { + mockUseAdvertiserUpdate = { + ...mockUseAdvertiserUpdate, + // @ts-expect-error Mock assertion of error + error: new Error(), + isLoading: false, + isSuccess: false, + }; + render(, { wrapper }); + + expect( + screen.getByText( + `Sorry, we're unable to increase your limits right now. Please try again in a few minutes.` + ) + ).toBeVisible(); + + const okBtn = screen.getByRole('button', { + name: 'Ok', + }); + userEvent.click(okBtn); + expect(mockOnRequestClose).toBeCalled(); + }); +}); diff --git a/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodErrorModal/__tests__/PaymentMethodErrorModal.spec.tsx b/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodErrorModal/__tests__/PaymentMethodErrorModal.spec.tsx new file mode 100644 index 000000000000..7604a3fe51f6 --- /dev/null +++ b/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodErrorModal/__tests__/PaymentMethodErrorModal.spec.tsx @@ -0,0 +1,19 @@ +import React, { PropsWithChildren } from 'react'; +import { render, screen } from '@testing-library/react'; +import PaymentMethodErrorModal from '../PaymentMethodErrorModal'; + +const wrapper = ({ children }: PropsWithChildren) =>
{children}
; + +describe('PaymentMethodErrorModal', () => { + it('should render the modal correctly', () => { + const props = { + errorMessage: 'error message', + isModalOpen: true, + onConfirm: jest.fn(), + title: 'title', + }; + render(, { wrapper }); + expect(screen.getByText('title')).toBeInTheDocument(); + expect(screen.getByText('error message')).toBeInTheDocument(); + }); +}); diff --git a/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodModal/__tests__/PaymentMethodModal.spec.tsx b/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodModal/__tests__/PaymentMethodModal.spec.tsx new file mode 100644 index 000000000000..5d11215dec3b --- /dev/null +++ b/packages/p2p-v2/src/components/Modals/PaymentMethods/PaymentMethodModal/__tests__/PaymentMethodModal.spec.tsx @@ -0,0 +1,61 @@ +import React, { PropsWithChildren } from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PaymentMethodModal from '../PaymentMethodModal'; + +const wrapper = ({ children }: PropsWithChildren) =>
{children}
; + +describe('PaymentMethodModal', () => { + it('should render the component correctly', () => { + render( + , + { wrapper } + ); + expect(screen.getByText('Payment Method Modal')).toBeInTheDocument(); + expect(screen.getByText('Payment Method Modal Description')).toBeInTheDocument(); + }); + it('should handle onclick when the yes, remove button is clicked', () => { + const onConfirm = jest.fn(); + render( + , + { wrapper } + ); + const confirmButton = screen.getByText('Yes, remove'); + userEvent.click(confirmButton); + expect(onConfirm).toHaveBeenCalled(); + }); + it('should handle onclick when the yes button is clicked', () => { + const onReject = jest.fn(); + render( + , + { wrapper } + ); + const rejectButton = screen.getByText('Yes'); + userEvent.click(rejectButton); + expect(onReject).toHaveBeenCalled(); + }); +}); diff --git a/packages/p2p-v2/src/components/PageReturn/PageReturn.tsx b/packages/p2p-v2/src/components/PageReturn/PageReturn.tsx index 73fa00ab398b..d9a4bb3d866a 100644 --- a/packages/p2p-v2/src/components/PageReturn/PageReturn.tsx +++ b/packages/p2p-v2/src/components/PageReturn/PageReturn.tsx @@ -13,7 +13,11 @@ type TPageReturnProps = { const PageReturn = ({ className = '', onClick, pageTitle }: TPageReturnProps) => { return (
- + {pageTitle}
); diff --git a/packages/p2p-v2/src/components/PageReturn/__tests__/PageReturn.spec.tsx b/packages/p2p-v2/src/components/PageReturn/__tests__/PageReturn.spec.tsx new file mode 100644 index 000000000000..74b3873087d1 --- /dev/null +++ b/packages/p2p-v2/src/components/PageReturn/__tests__/PageReturn.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import PageReturn from '../PageReturn'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const mockOnClick = jest.fn(); +describe('PageReturn', () => { + it('should render the title and behaviour of return correctly', () => { + render(); + + expect(screen.getByText('Cashier P2P')).toBeVisible(); + const returnBtn = screen.getByTestId('dt_p2p_v2_page_return_btn'); + userEvent.click(returnBtn); + expect(mockOnClick).toBeCalled(); + }); +}); diff --git a/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx b/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx new file mode 100644 index 000000000000..233f69dbf3bc --- /dev/null +++ b/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx @@ -0,0 +1,178 @@ +import * as React from 'react'; +import { APIProvider, p2p, useAuthentication, useSettings } from '@deriv/api'; +import { renderHook } from '@testing-library/react-hooks'; +import useAdvertiserStats from '../useAdvertiserStats'; + +const mockUseSettings = useSettings as jest.MockedFunction; +const mockUseAuthentication = useAuthentication as jest.MockedFunction; +const mockUseAdvertiserInfo = p2p.advertiser.useGetInfo as jest.MockedFunction; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + p2p: { + advertiser: { + useGetInfo: jest.fn().mockReturnValue({ + data: { + currency: 'USD', + }, + isLoading: false, + isSuccess: true, + }), + }, + }, + useAuthentication: jest.fn().mockReturnValue({ + data: { + currency: 'USD', + }, + isLoading: false, + isSuccess: true, + }), + useSettings: jest.fn().mockReturnValue({ + data: { + currency: 'USD', + }, + isLoading: false, + isSuccess: true, + }), +})); + +describe('useAdvertiserStats', () => { + test('should not return data when useSettings and useAuthentication is still fetching', () => { + mockUseAuthentication.mockReturnValueOnce({ + ...mockUseAuthentication, + isSuccess: false, + }); + mockUseSettings.mockReturnValueOnce({ + ...mockUseSettings, + isSuccess: false, + }); + mockUseAdvertiserInfo.mockReturnValueOnce({ + ...mockUseAdvertiserInfo, + isSuccess: false, + }); + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data).toBe(undefined); + }); + test('should return the correct information', () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + mockUseSettings.mockReturnValueOnce({ + data: { first_name: 'Jane', last_name: 'Doe' }, + }); + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + buy_orders_count: 10, + created_time: 1698034883, + partner_count: 1, + sell_orders_count: 5, + }, + }); + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data.fullName).toBe('Jane Doe'); + expect(result.current.data.tradePartners).toBe(1); + expect(result.current.data.buyOrdersCount).toBe(10); + expect(result.current.data.sellOrdersCount).toBe(5); + expect(result.current.data.daysSinceJoined).toBe(119); + }); + test('should return the correct total count and lifetime', () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + buy_orders_amount: 10, + buy_orders_count: 10, + partner_count: 1, + sell_orders_amount: 50, + sell_orders_count: 5, + total_orders_count: 30, + total_turnover: 100, + }, + }); + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data.totalOrders).toBe(15); + expect(result.current.data.totalOrdersLifetime).toBe(30); + expect(result.current.data.tradeVolume).toBe(60); + expect(result.current.data.tradeVolumeLifetime).toBe(100); + }); + test('should return the correct rates and limits', () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + buy_completion_rate: 1.4, + daily_buy: 10, + daily_buy_limit: 100, + daily_sell: 40, + daily_sell_limit: 50, + sell_completion_rate: 2.4, + upgradable_daily_limits: 1, + }, + }); + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data.buyCompletionRate).toBe(1.4); + expect(result.current.data.sellCompletionRate).toBe(2.4); + expect(result.current.data.dailyAvailableBuyLimit).toBe(90); + expect(result.current.data.dailyAvailableSellLimit).toBe(10); + expect(result.current.data.isEligibleForLimitUpgrade).toBe(true); + }); + test('should return the correct buy/release times', () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + buy_time_avg: 150, + release_time_avg: 40, + }, + }); + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data.averagePayTime).toBe(3); + expect(result.current.data.averageReleaseTime).toBe(1); + }); + test('should return the correct verification statuses', () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + full_verification: false, + is_approved: false, + }, + }); + mockUseAuthentication.mockReturnValueOnce({ + data: { + document: { + status: 'verified', + }, + identity: { + status: 'pending', + }, + }, + }); + const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(result.current.data.isAddressVerified).toBe(true); + expect(result.current.data.isIdentityVerified).toBe(false); + + mockUseAdvertiserInfo.mockReturnValueOnce({ + data: { + full_verification: true, + is_approved: true, + }, + }); + mockUseAuthentication.mockReturnValueOnce({ + data: { + document: { + status: 'verified', + }, + identity: { + status: 'pending', + }, + }, + }); + const { result: verifiedResult } = renderHook(() => useAdvertiserStats(), { wrapper }); + + expect(verifiedResult.current.data.isAddressVerified).toBe(true); + expect(verifiedResult.current.data.isIdentityVerified).toBe(true); + }); +}); diff --git a/packages/p2p-v2/src/hooks/__tests__/usePoiPoaStatus.spec.tsx b/packages/p2p-v2/src/hooks/__tests__/usePoiPoaStatus.spec.tsx new file mode 100644 index 000000000000..b51f9c64492a --- /dev/null +++ b/packages/p2p-v2/src/hooks/__tests__/usePoiPoaStatus.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { APIProvider, useGetAccountStatus } from '@deriv/api'; +import { renderHook } from '@testing-library/react-hooks'; +import usePoiPoaStatus from '../usePoiPoaStatus'; + +const mockUseGetAccountStatus = useGetAccountStatus as jest.MockedFunction; +const wrapper = ({ children }: { children: JSX.Element }) => {children}; +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useGetAccountStatus: jest.fn().mockReturnValue({ + data: { + authentication: { + document: { + status: 'pending', + }, + identity: { + status: 'pending', + }, + }, + p2p_poa_required: true, + }, + }), +})); + +describe('usePoiPoaStatus', () => { + it('should return the correct pending verification statuses', () => { + const { result } = renderHook(() => usePoiPoaStatus(), { wrapper }); + + expect(result.current.data).toStrictEqual({ + isP2PPoaRequired: true, + isPoaPending: true, + isPoaVerified: false, + isPoiPending: true, + isPoiVerified: false, + poaStatus: 'pending', + poiStatus: 'pending', + }); + }); + it('should return the correct verified verification statuses', () => { + mockUseGetAccountStatus.mockReturnValueOnce({ + data: { + authentication: { + document: { + status: 'verified', + }, + identity: { + status: 'verified', + }, + }, + p2p_poa_required: false, + }, + }); + const { result } = renderHook(() => usePoiPoaStatus(), { wrapper }); + + expect(result.current.data).toStrictEqual({ + isP2PPoaRequired: false, + isPoaPending: false, + isPoaVerified: true, + isPoiPending: false, + isPoiVerified: true, + poaStatus: 'verified', + poiStatus: 'verified', + }); + }); + it('should return undefined if data is not available', () => { + mockUseGetAccountStatus.mockReturnValueOnce({ + data: undefined, + }); + const { result } = renderHook(() => usePoiPoaStatus(), { wrapper }); + + expect(result.current.data).toBeUndefined(); + }); +}); diff --git a/packages/p2p-v2/src/hooks/useAdvertiserStats.ts b/packages/p2p-v2/src/hooks/useAdvertiserStats.ts index aaba0c464a64..7f3cf984a7cb 100644 --- a/packages/p2p-v2/src/hooks/useAdvertiserStats.ts +++ b/packages/p2p-v2/src/hooks/useAdvertiserStats.ts @@ -56,10 +56,12 @@ const useAdvertiserStats = (advertiserId?: string) => { ), /** The advertiser's full name */ - fullName: (settings?.first_name || '') + (settings?.last_name || ''), + fullName: `${settings?.first_name || ''} ${settings?.last_name || ''}`, /** Checks if the advertiser has completed proof of address verification */ - isAddressVerified: isAdvertiser ? data?.full_verification : authenticationStatus?.document?.status, + isAddressVerified: isAdvertiser + ? data?.full_verification + : authenticationStatus?.document?.status === 'verified', /** Checks if the user is already an advertiser */ isAdvertiser, @@ -68,7 +70,9 @@ const useAdvertiserStats = (advertiserId?: string) => { isEligibleForLimitUpgrade: Boolean(data?.upgradable_daily_limits), /** Checks if the advertiser has completed proof of identity verification */ - isIdentityVerified: isAdvertiser ? data?.full_verification : authenticationStatus?.identity?.status, + isIdentityVerified: isAdvertiser + ? data?.full_verification + : authenticationStatus?.identity?.status === 'verified', /** The percentage of completed orders out of total orders as a seller within the past 30 days. */ sellCompletionRate: data?.sell_completion_rate || 0, diff --git a/packages/p2p/src/components/modal-manager/modals/nickname-modal/nickname-modal.tsx b/packages/p2p/src/components/modal-manager/modals/nickname-modal/nickname-modal.tsx index b2be0bc99047..47f256ef2fc5 100644 --- a/packages/p2p/src/components/modal-manager/modals/nickname-modal/nickname-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/nickname-modal/nickname-modal.tsx @@ -48,10 +48,10 @@ const NicknameModal = ({ onCancel, onConfirm, should_hide_close_btn = false }: T > - + - - + +
diff --git a/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/__tests__/unsupported-contract-modal.spec.tsx b/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/__tests__/unsupported-contract-modal.spec.tsx index 271e95ea366d..b22b062ff195 100644 --- a/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/__tests__/unsupported-contract-modal.spec.tsx +++ b/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/__tests__/unsupported-contract-modal.spec.tsx @@ -23,5 +23,6 @@ describe('UnsupportedContractModal', () => { }); expect(screen.getByText(/You’ve selected a trade type that is currently unsupported/i)).toBeInTheDocument(); + expect(screen.getByText(/Go to Deriv.com/i)).toBeInTheDocument(); }); }); diff --git a/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/unsupported-contract-modal.tsx b/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/unsupported-contract-modal.tsx index d9e7b9423509..cbf6d4d48365 100644 --- a/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/unsupported-contract-modal.tsx +++ b/packages/trader/src/App/Components/Elements/Modals/UnsupportedContractModal/unsupported-contract-modal.tsx @@ -21,7 +21,7 @@ const UnsupportedContractModal = observer( { }; const unsupportedContractOnClose = () => { - const portfoliows_url = urlFor('user/portfoliows', { legacy: true }); - window.open(portfoliows_url, '_blank'); + window.open(getStaticUrl('/')); unsupportedContractOnConfirm(); }; diff --git a/packages/tradershub/package.json b/packages/tradershub/package.json index a84aa6d36ac9..e603af77a5ed 100644 --- a/packages/tradershub/package.json +++ b/packages/tradershub/package.json @@ -17,7 +17,7 @@ "@deriv/integration": "^1.0.0", "@deriv/library": "^1.0.0", "@deriv/quill-icons": "^1.18.3", - "@deriv-com/ui": "1.5.3", + "@deriv-com/ui": "1.8.1", "@deriv/react-joyride": "^2.6.2", "@deriv/utils": "^1.0.0", "@tanstack/react-table": "^8.10.3",