From 43a8075ad81bc6c08cfa8f9fe77cf61908809207 Mon Sep 17 00:00:00 2001 From: Farabi <102643568+farabi-deriv@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:14:11 +0800 Subject: [PATCH] Suisin/phone number verification (#16886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update master (#16881) * [UPM-1474]/evgeniy/passkey remove without verification (#16764) * chore: [UPM-1474]/evgeniy/passkey remove without verification * fix: import and typo * fix: error modal overlapping, no paskey page show when no passkey after removing * chore: test cases * refactor: remove usequery from getpasskeys * fix: failing test * fix: failing tests * fix: test * fix: wrong condition to save empty passkey array * feat(cashier): :sparkles: support crypto withdrawal transaction redirection from deriv go (#16802) --------- Co-authored-by: yauheni-deriv <103182683+yauheni-deriv@users.noreply.github.com> Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> * Suisin/utkarsha/upm819/phone verification flow (#16882) * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent * Suisin/utkarsha/upm819/phone verification flow (#16157) * feat: move timer to diff component * refactor: replace timer code with component * test: testcase for resend-code-timer * chore: add test case on cancel phone verification modal and move to a seperate component * chore: update test case for phone-verification-page * chore: update codes based on comments * chore: update data testid * fix: change text for email and phone, add TODOs * chore: edit testcases * chore: change prop to text * test: update testcase * chore: use ReturnType instead of NodeJS.timer * chore: update quill-ui package and remove previous build config edits * feat: integrate phone number challenge api * chore: add comments * chore: add ThemeProvider in phone verification * chore: use states instead of useRef and update testcase * test: added testcases for the hook * chore: add phone otp screen * chore: update based on comments * chore: remove unused store in test case * chore: add sms and whatsapp to 1 constant file * chore: update code based on comments * test: add tescases for counter change and title change * chore: update code based on comments * test: use useRealTimers * test: mock useGetPhoneNumberOTP * chore: update TODO comments * chore: update code * chore: update function for WhatsApp display * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent --------- Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * chore: update growthbook value in AppContent * chore: remove is_phone_number_verification_enabled * chore: solve redirect not working issue * chore: update packages for deriv-com/quill-ui * Suisin/utkarsha/upm819/phone verification flow (#16243) * fix: change text for email and phone, add TODOs * chore: edit testcases * chore: change prop to text * test: update testcase * chore: use ReturnType instead of NodeJS.timer * chore: update quill-ui package and remove previous build config edits * feat: integrate phone number challenge api * chore: add comments * chore: add ThemeProvider in phone verification * chore: use states instead of useRef and update testcase * test: added testcases for the hook * chore: add phone otp screen * chore: update based on comments * chore: remove unused store in test case * chore: add sms and whatsapp to 1 constant file * chore: update code based on comments * test: add tescases for counter change and title change * chore: update code based on comments * test: use useRealTimers * test: mock useGetPhoneNumberOTP * chore: update TODO comments * chore: update code * chore: update function for WhatsApp display * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent * Suisin/utkarsha/upm819/phone verification flow (#16157) * feat: move timer to diff component * refactor: replace timer code with component * test: testcase for resend-code-timer * chore: add test case on cancel phone verification modal and move to a seperate component * chore: update test case for phone-verification-page * chore: update codes based on comments * chore: update data testid * fix: change text for email and phone, add TODOs * chore: edit testcases * chore: change prop to text * test: update testcase * chore: use ReturnType instead of NodeJS.timer * chore: update quill-ui package and remove previous build config edits * feat: integrate phone number challenge api * chore: add comments * chore: add ThemeProvider in phone verification * chore: use states instead of useRef and update testcase * test: added testcases for the hook * chore: add phone otp screen * chore: update based on comments * chore: remove unused store in test case * chore: add sms and whatsapp to 1 constant file * chore: update code based on comments * test: add tescases for counter change and title change * chore: update code based on comments * test: use useRealTimers * test: mock useGetPhoneNumberOTP * chore: update TODO comments * chore: update code * chore: update function for WhatsApp display * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent --------- Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * chore: update growthbook value in AppContent * chore: remove is_phone_number_verification_enabled * chore: solve redirect not working issue * chore: update packages for deriv-com/quill-ui --------- Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * master update (#16253) * fix: removed clean urls from vercel dr json (#16249) * Revert "[WALL] george / WALL-4522 / Add ce_cashier_deposit_onboarding_form an…" (#16246) This reverts commit a10f85ced2d3ac9f29137bb1f1a72c5309074651. * [DTRA] Maryia/DTRA-1546/fix: [V2] style & animation for Digits Current spot price + active_symbols request (#16225) * fix: styles & animation for current spot for digit trade types * fix: active_symbols call for rise/fall and higher/lower * fix: useActiveSymbols usage * fix: active_symbols call with relevant barrier_category + error handling same like in prod --------- Co-authored-by: nada-deriv <122768621+nada-deriv@users.noreply.github.com> Co-authored-by: Maryia <103177211+maryia-deriv@users.noreply.github.com> * chore: update usePhoneNumberVerificationSetTimer to use useServerTime * chore: update to use useGrowthbookGetFeatureFlag * Suisin/utkarsha/upm819/phone verification flow (#16266) * test: added testcases for the hook * chore: add phone otp screen * chore: update based on comments * chore: remove unused store in test case * chore: add sms and whatsapp to 1 constant file * chore: update code based on comments * test: add tescases for counter change and title change * chore: update code based on comments * test: use useRealTimers * test: mock useGetPhoneNumberOTP * chore: update TODO comments * chore: update code * chore: update function for WhatsApp display * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent * Suisin/utkarsha/upm819/phone verification flow (#16157) * feat: move timer to diff component * refactor: replace timer code with component * test: testcase for resend-code-timer * chore: add test case on cancel phone verification modal and move to a seperate component * chore: update test case for phone-verification-page * chore: update codes based on comments * chore: update data testid * fix: change text for email and phone, add TODOs * chore: edit testcases * chore: change prop to text * test: update testcase * chore: use ReturnType instead of NodeJS.timer * chore: update quill-ui package and remove previous build config edits * feat: integrate phone number challenge api * chore: add comments * chore: add ThemeProvider in phone verification * chore: use states instead of useRef and update testcase * test: added testcases for the hook * chore: add phone otp screen * chore: update based on comments * chore: remove unused store in test case * chore: add sms and whatsapp to 1 constant file * chore: update code based on comments * test: add tescases for counter change and title change * chore: update code based on comments * test: use useRealTimers * test: mock useGetPhoneNumberOTP * chore: update TODO comments * chore: update code * chore: update function for WhatsApp display * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent --------- Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * chore: update growthbook value in AppContent * chore: remove is_phone_number_verification_enabled * chore: solve redirect not working issue * chore: update packages for deriv-com/quill-ui * fix: removed clean urls from vercel dr json (#16249) * Revert "[WALL] george / WALL-4522 / Add ce_cashier_deposit_onboarding_form an…" (#16246) This reverts commit a10f85ced2d3ac9f29137bb1f1a72c5309074651. * [DTRA] Maryia/DTRA-1546/fix: [V2] style & animation for Digits Current spot price + active_symbols request (#16225) * fix: styles & animation for current spot for digit trade types * fix: active_symbols call for rise/fall and higher/lower * fix: useActiveSymbols usage * fix: active_symbols call with relevant barrier_category + error handling same like in prod * chore: replace localize import with new library (#16140) * chore: replace localize import with new library * chore: removed unused component * chore: update usePhoneNumberVerificationSetTimer to use useServerTime * [WALL] Lubega / WALL-4549 / Wallets initial translations setup (#16158) * feat: draft wallet translations * feat: initial wallets translations setup * chore: clean up code * fix: resolve error * fix: resolve error * chore: update text component * fix: env variables and language switcher * [WALL] Lubega/ WALL-4549 / Wallets multi language support (#16069) * feat: draft wallet translations * feat: initial wallets translations setup * chore: clean up code * fix: resolve error * fix: resolve error * chore: update text component * fix: env variables and language switcher * fix: update locked scenarios * [WALL] Lubega / Wallets translations update (#16112) * feat: draft wallet translations * feat: initial wallets translations setup * chore: clean up code * fix: resolve error * fix: resolve error * chore: update text component * fix: env variables and language switcher * Suisin/fix: text not bold in email and password page (#16094) * fix: text not bold in email and password page * chore: update package version to use specific version * Fasih/COJ-1275/ Implemented lazy load (#16020) * chore: implemented lazy load on financial assessment and trading assesment * chore: working on personal details * chore: removed lazy load from personal details --------- Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * [WALL] george / WALL-4402 / feat(wallets): ✨ add analytics to track wallets events (#16004) * feat(wallets): :sparkles: add wallet migration analytic * chore: :mute: suppress ts error * chore: align with master * fix: prettified code * chore: fix isOpen condition appear twice * fix: update locked scenarios --------- Co-authored-by: Sui Sin <103026762+suisin-deriv@users.noreply.github.com> Co-authored-by: fasihali-deriv <121229483+fasihali-deriv@users.noreply.github.com> Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> * fix: update github workflow * fix: getWalletHeaderButtons format * Update index.js * Update index.js * Update index.js * Update index.js * Update index.js * chore: remove eslint auto format * chore: test remove line * chore: test remove line * chore: test remove line * chore: test remove line * chore: test remove line * chore: update deriv-com/translations version --------- Co-authored-by: Sui Sin <103026762+suisin-deriv@users.noreply.github.com> Co-authored-by: fasihali-deriv <121229483+fasihali-deriv@users.noreply.github.com> Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> Co-authored-by: nijil-deriv * translations: 📚 sync translations with crowdin (#16262) Co-authored-by: DerivFE * ci: change env usage (#16264) * Ako/ override installCommand of vercel (#16257) * build: override installcommand of vercel * build: override installcommand of vercel (#16256) * ci: fix the staging vercel env * chore: update to use useGrowthbookGetFeatureFlag --------- Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> Co-authored-by: nada-deriv <122768621+nada-deriv@users.noreply.github.com> Co-authored-by: Maryia <103177211+maryia-deriv@users.noreply.github.com> Co-authored-by: Likhith Kolayari <98398322+likhith-deriv@users.noreply.github.com> Co-authored-by: lubega-deriv <142860499+lubega-deriv@users.noreply.github.com> Co-authored-by: fasihali-deriv <121229483+fasihali-deriv@users.noreply.github.com> Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> Co-authored-by: nijil-deriv Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: DerivFE Co-authored-by: Ali(Ako) Hosseini * chore: move setLoading to top of the function * chore: update to use WS instead of useServerTimer * chore: fix cd /Users/suisin/Documents/deriv-apppersonal-details-form test case * chore: update otp-verification test case * chore: fix notification tray not closing issue * Suisin/utkarsha/upm819/phone verification flow (#16313) * chore: fix test case fail for confirm-phone-number * chore: resolve failling test case * chore: resolve conflicts otp-verification-component * fix: prettier issue scss format * chore: add phone verification successful modal * chore: edit classname for verified modal * chore: update test case describe name and ui store value * chore: move successful modal inside phone-number-verification-page * chore: update phone-number-verified-modal and remove ui store values * chore: revert unused changes * chore: add routes to personal details * chore: fix matchMedia failling in test case * chore: update test case title * chore: replace modal with quill-ui modal * chore: update test case to pass first for phone number verified modal * fix: test case * chore: update code base on comments * chore: resolve integration test failling * chore: update code based on comments * chore: add didnt get the code modal for phone verification * chore: update test case and files for didnt get the code modal * chore: update code based on comments * chore: update test case based on comments * chore: update helpers.ts based on comments * chore: update based on comments * chore: move convertPhoneTypeDisplay to correct folders * chore: update resend-code-timer test case to use jest.advanceTimer * chore: update show to show_otp_verification * chore: update to use localise function * chore: create verification-link-expired-modal * chore: update all quill-ui packages * chore: update test case title * chore: update package version for quill-ui * chore: create useGetEmailVerificationOTP hooks * chore: implement callback for hooks * chore: try to add InputGroupButton for phone-verification * chore: update code based on comments * chore: remove useGetEmailVerificationOTP and use useVerifyEmail instead * chore: remove Input field from component and use quill-ui TextField and InputGroupButton * chore: update scss * chore: fix test failing for resend-code-timer.spec.tsx * chore: implement phone number otp api hooks * chore: update test case for useSendOTPVerificationCode * chore: update otp-verification.spec.tsx * chore: update title for otp verification and add test for Phone number verified modal * chore: update naming to is_phone_number_verified based on comment * chore: update localize in hooks * fix: type error * chore: resolve build fail issue * chore: add localize at useSendOTPVerificationCode * chore: implement error status when phone number is invalid * chore: fix test case for confirm phone number * chore: rename test case title and hooks based on comment * chore: moved validePhoneNumber to a constant file * chore: add error screens for email otp invalid * chore: remove unused package in hooks * chore: added useSetSettings hooks and change logic using new implemented hooks * chore: update test case and localStorage setItem * chore: rename handleError to formatError * chore: change useSetSettings to useSettings and modify the hooks * chore: update based on comments * chore: remove conditional check for handleError and make it into 2 different formatError * chore: include comments in hooks for requestSMS and requestWhatsapp * chore: update useSEndOTPVerificationCode test case * chore: add routing from email verification link * chore: remove comments * chore: implement timer from BE * chore: update code based on comment * chore: use 1 useState for otp request * chore: move to 1 useState based on comment * chore: rename phone_number_verification and move setIsLoading in if else statement based on comment * chore: move setTimer useState into otp-verification * chore: need to use React.useCallback to solve blinking issue for reInitalizeGetSettings * chore: remove setIsButtonDisabled in validate next_email_otp_request function * chore: update code based on comments * chore: update to use is_email_verified * chore: add WS call when redirect from notification tray * chore: remove unused comment * chore: update quill-ui in package.json * chore: added Math.round for minutes timer * chore: update code to have better user flow * chore: realign code for BE integration * chore: add timer to personal-details, expired modal and notification * chore: remove unused dayjs in account package * chore: update quill-ui version and updated codes for expired-link-modal * chore: update verification link expired modal * chore: add timer countdown snackbar in confirm your phone number page * chore: update commented paragraph and remove TODOs * chore: update test case and remove ts-expect-error * chore: remove unused ...rest * chore: include !! to phone_number_verification.verified * chore: update verify button to clear all possible stored value * chore: update code to fix demo session bugs * chore: remove is_mobile from ui-store and update to latest useDevice * chore: fix test case failling and update code based on comments * chore: remove phone_number_verify?.verified logic check in personal details * chore: fix test case and remove unused imports * chore: update to fix test case * chore: update code based on comments * chore: update test case * chore: update code based on comments * chore: revert icons file * chore: update code based on comments * chore: update packages version and fix useSetting based on comments * chore: update code based on comments * chore: update verify-button.spec.tsx based on comment * chore: update quill-ui version and code based on comments * chore: update positions test case with scrollTo fix * chore: remove @ts-expected-error * fix: console log verified not found issue and clear otp after click resend code * chore: fix authorize issue * chore: update quill-ui version for console log fix * chore: fix verification link expired rerendering issue * fix: clicks on resend link or send via whatsapp redirect user back to confirm phone number page * chore: have to include color black in button so the underline will be black * chore: update quill-ui version * chore: update to include type number in InputGroupButton * chore: update useVerifyEmail hooks to fix button disabled issue * chore: should run timer also if error is returned from BE * fix: console error on clicking verify button in personal details section * chore: fix unmounted console log issue * chore: use useEffect directly from react package * chore: fix test case fail issue * chore: fix link-expired-modal cause added a boolean value in the hooks * chore: replace TextField with TextFieldAddOn for confirm your phone number page * chore: create new functional call in useVerifyEmail and remove deprecated tag * chore: fix console log issue after clicking on verify button * fix: legacywonicon not displaying properly by updating the version * chore: update quill-ui version to fix input validation issue * chore: update quill-ui verison to fix dark mode + issue * chore: use lazyLoading for phone number verification * chore: fix verify-button test case based on comment * chore: update codes based on comment * fix: resolve conflicts * chore: revert phone number verification changes with master * chore: make verify button disabled once user edit personal details and added growthbook * chore: scss changes to fix tablet view issue * chore: hide notification and show demo message when user is in demo account * chore: implement growthbook in AppContent and added client store for it * chore: change to redirect user back to personal details once they switch to demo * chore: move popover tablet view from right to top * chore: hide verify button when phone number changes * chore: update growthbook usage based on comment * chore: update useEffect in AppContent * Suisin/utkarsha/upm819/phone verification flow (#16157) * feat: move timer to diff component * refactor: replace timer code with component * test: testcase for resend-code-timer * chore: add test case on cancel phone verification modal and move to a seperate component * chore: update test case for phone-verification-page * chore: update codes based on comments * chore: update data testid * fix: change text for email and phone, add TODOs * chore: edit testcases * chore: change prop to text * test: update testcase * chore: use ReturnType instead of NodeJS.timer *… * chore: updated shared package utils version * [PNV] - (UPM1501) Suisin/chore: update stepper for pnv flow (#16909) * chore: update stepper for pnv flow * chore: fix test case fail * chore: change verify access to verification needed * chore: update / to of in stepper * [PNV] - (UPM1500) Suisin/chore: create expiry count down timer (#16921) * chore: create expiry count down timer * chore: update color to regex instead of rgba * [PNV] - (UPM1498) Suisin/chore: strip any characters other than + and numbers for phone number (#16887) * chore: strip any characters other than + and numbers for phone number * chore: update logic based on comment * chore: update striping non numeric characters in personal details only * chore: change regex in confirm-your-phone page to strip out non numeric characters * chore: update regex to stripped out phone number external characters * chore: update regex to a better one to accept only digit * chore: replace regex in confirm your phone number screen * chore: update validation checks for phone number in personal details settings * chore: revert error message to previous content * chore: on click verify button should save others personal details content (#16924) * [PNV] - (UPM1499) Suisin/chore: on click notification should directly go into phone number ver… (#16898) * chore: on click notification should directly go into phone number verification flow * chore: notification should not display during pnv flow * [PNV] - (UPM1505) Suisin: redirect user to where they started (#16918) * chore: update routes to redirect user to where they came from * chore: update test case for phone-number-verified-modal * chore: update phone number verified modal content * chore: update variables name based on comment --------- Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> * [PNV] - (UPM1502) Suisin/chore: update content based on latest design (#16934) * chore: update content based on latest design * chore: update minutes to minute based on comment * chore: update timer seconds to have singular value checks * [PNV] UPM1635 Suisin/chore: fix growthbook FF off pnv routing issue (#16999) * chore: fix growthbook FF off pnv routing issue * chore: fix console log issue * [PNV] (UPM) Suisin/ chore: update title for step 1 email verification (#16998) * chore: update title for step 1 email verification * chore: update test case title * chore: update notification title --------- Co-authored-by: amina-deriv <84661147+amina-deriv@users.noreply.github.com> Co-authored-by: yauheni-deriv <103182683+yauheni-deriv@users.noreply.github.com> Co-authored-by: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> Co-authored-by: Sui Sin <103026762+suisin-deriv@users.noreply.github.com> Co-authored-by: utkarsha-deriv Co-authored-by: amina-deriv Co-authored-by: nada-deriv <122768621+nada-deriv@users.noreply.github.com> Co-authored-by: Maryia <103177211+maryia-deriv@users.noreply.github.com> Co-authored-by: Likhith Kolayari <98398322+likhith-deriv@users.noreply.github.com> Co-authored-by: lubega-deriv <142860499+lubega-deriv@users.noreply.github.com> Co-authored-by: fasihali-deriv <121229483+fasihali-deriv@users.noreply.github.com> Co-authored-by: nijil-deriv Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: DerivFE Co-authored-by: Ali(Ako) Hosseini Co-authored-by: rupato-deriv <97010868+rupato-deriv@users.noreply.github.com> Co-authored-by: maryia-matskevich-deriv <103181650+maryia-matskevich-deriv@users.noreply.github.com> --- package-lock.json | 1 + packages/account/jest.config.js | 3 +- .../src/Configs/personal-details-config.ts | 7 +- .../account/src/Constants/personal-details.ts | 2 +- .../account/src/Constants/routes-config.tsx | 11 + .../src/Containers/__tests__/routes.spec.tsx | 13 + packages/account/src/Containers/routes.tsx | 11 +- packages/account/src/Helpers/utils.tsx | 7 + .../__tests__/personal-details-form.spec.tsx | 23 +- .../__tests__/verify-button.spec.tsx | 130 ++++++++++ .../PersonalDetails/personal-details-form.tsx | 100 +++++++- .../Profile/PersonalDetails/validation.ts | 19 +- .../PersonalDetails/verify-button.scss | 7 + .../Profile/PersonalDetails/verify-button.tsx | 116 +++++++++ .../cancel-phone-verification-modal.spec.tsx | 79 ++++++ .../__test__/confirm-phone-number.spec.tsx | 170 +++++++++++++ .../__test__/cool-down-period-modal.spec.tsx | 46 ++++ .../didnt-get-the-code-modal.spec.tsx | 104 ++++++++ .../__test__/otp-verification.spec.tsx | 200 ++++++++++++++++ .../phone-number-verified-modal.spec.tsx | 59 +++++ .../__test__/phone-verification-card.spec.tsx | 44 ++++ .../__test__/phone-verification-page.spec.tsx | 106 ++++++++ .../__test__/resend-code-timer.spec.tsx | 113 +++++++++ .../__test__/session-timeout-modal.spec.tsx | 48 ++++ .../__test__/validation.spec.tsx | 57 +++++ .../verification-link-expired-modal.spec.tsx | 83 +++++++ .../cancel-phone-verification-modal.tsx | 95 ++++++++ .../confirm-phone-number.tsx | 179 ++++++++++++++ .../cool-down-period-modal.tsx | 48 ++++ .../didnt-get-the-code-modal.tsx | 106 ++++++++ .../Profile/PhoneVerification/index.ts | 3 + .../PhoneVerification/otp-verification.tsx | 226 ++++++++++++++++++ .../phone-number-verified-modal.tsx | 74 ++++++ .../phone-verification-card.tsx | 34 +++ .../phone-verification-page.tsx | 143 +++++++++++ .../PhoneVerification/phone-verification.scss | 149 ++++++++++++ .../PhoneVerification/resend-code-timer.tsx | 107 +++++++++ .../session-timeout-modal.tsx | 53 ++++ .../Profile/PhoneVerification/validation.ts | 25 ++ .../verification-link-expired-modal.tsx | 93 +++++++ packages/account/src/Styles/account.scss | 4 + packages/api/types.ts | 91 +++++++ .../form-submit-error-message.tsx | 29 ++- .../open-livechat-link/open-livechat-link.tsx | 10 +- packages/core/src/App/AppContent.tsx | 14 +- .../notifications-list.tsx | 4 + .../src/App/Containers/Redirect/redirect.jsx | 11 + .../Containers/app-notification-messages.jsx | 1 + packages/core/src/Services/socket-general.js | 1 + packages/core/src/Stores/client-store.js | 18 +- .../core/src/Stores/notification-store.js | 40 +++- packages/core/src/Stores/ui-store.js | 30 +++ .../_common/components/account-common.scss | 16 +- packages/hooks/package.json | 2 + .../useIsPhoneNumberVerified.spec.tsx | 33 +++ ...oneNumberVerificationSessionTimer.spec.tsx | 83 +++++++ ...sePhoneNumberVerificationSetTimer.spec.tsx | 76 ++++++ .../usePhoneVerificationAnalytics.spec.tsx | 33 +++ .../useRequestPhoneNumberOTP.spec.tsx | 206 ++++++++++++++++ .../useSendOTPVerificationCode.spec.tsx | 148 ++++++++++++ .../hooks/src/__tests__/useSettings.spec.tsx | 94 ++++++++ packages/hooks/src/index.ts | 7 + .../hooks/src/useGrowthbookGetFeatureValue.ts | 6 +- .../hooks/src/useIsPhoneNumberVerified.ts | 14 ++ .../usePhoneNumberVerificationSessionTimer.ts | 72 ++++++ .../usePhoneNumberVerificationSetTimer.tsx | 95 ++++++++ .../src/usePhoneVerificationAnalytics.ts | 22 ++ .../hooks/src/useRequestPhoneNumberOTP.tsx | 147 ++++++++++++ .../hooks/src/useSendOTPVerificationCode.tsx | 115 +++++++++ packages/hooks/src/useSettings.ts | 31 +++ packages/hooks/src/useVerifyEmail.ts | 9 +- .../integration/src/mocks/auth/getSettings.ts | 54 +++++ packages/shared/package.json | 1 + packages/shared/src/styles/constants.scss | 1 + packages/shared/src/utils/constants/index.ts | 1 + .../constants/phone-number-verification.ts | 13 + packages/shared/src/utils/routes/routes.ts | 1 + packages/stores/src/mockStore.ts | 11 + packages/stores/types.ts | 18 ++ 79 files changed, 4393 insertions(+), 63 deletions(-) create mode 100644 packages/account/src/Sections/Profile/PersonalDetails/__tests__/verify-button.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PersonalDetails/verify-button.scss create mode 100644 packages/account/src/Sections/Profile/PersonalDetails/verify-button.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/cancel-phone-verification-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/confirm-phone-number.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/cool-down-period-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/didnt-get-the-code-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/otp-verification.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-number-verified-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-card.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-page.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/resend-code-timer.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/session-timeout-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/validation.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/__test__/verification-link-expired-modal.spec.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/cancel-phone-verification-modal.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/confirm-phone-number.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/cool-down-period-modal.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/didnt-get-the-code-modal.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/index.ts create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/otp-verification.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/phone-number-verified-modal.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/phone-verification-card.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/phone-verification-page.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/phone-verification.scss create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/resend-code-timer.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/session-timeout-modal.tsx create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/validation.ts create mode 100644 packages/account/src/Sections/Profile/PhoneVerification/verification-link-expired-modal.tsx create mode 100644 packages/hooks/src/__tests__/useIsPhoneNumberVerified.spec.tsx create mode 100644 packages/hooks/src/__tests__/usePhoneNumberVerificationSessionTimer.spec.tsx create mode 100644 packages/hooks/src/__tests__/usePhoneNumberVerificationSetTimer.spec.tsx create mode 100644 packages/hooks/src/__tests__/usePhoneVerificationAnalytics.spec.tsx create mode 100644 packages/hooks/src/__tests__/useRequestPhoneNumberOTP.spec.tsx create mode 100644 packages/hooks/src/__tests__/useSendOTPVerificationCode.spec.tsx create mode 100644 packages/hooks/src/__tests__/useSettings.spec.tsx create mode 100644 packages/hooks/src/useIsPhoneNumberVerified.ts create mode 100644 packages/hooks/src/usePhoneNumberVerificationSessionTimer.ts create mode 100644 packages/hooks/src/usePhoneNumberVerificationSetTimer.tsx create mode 100644 packages/hooks/src/usePhoneVerificationAnalytics.ts create mode 100644 packages/hooks/src/useRequestPhoneNumberOTP.tsx create mode 100644 packages/hooks/src/useSendOTPVerificationCode.tsx create mode 100644 packages/hooks/src/useSettings.ts create mode 100644 packages/integration/src/mocks/auth/getSettings.ts create mode 100644 packages/shared/src/utils/constants/phone-number-verification.ts diff --git a/package-lock.json b/package-lock.json index 28fb4bf7de8a..27f1c45201b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "css-hot-loader": "^1.4.4", "css-loader": "^5.0.1", "css-minimizer-webpack-plugin": "^3.0.1", + "dayjs": "^1.11.11", "deep-diff": "^1.0.2", "dompurify": "^3.1.0", "dotenv": "^8.2.0", diff --git a/packages/account/jest.config.js b/packages/account/jest.config.js index ee7834a92264..1e3d737cc6df 100644 --- a/packages/account/jest.config.js +++ b/packages/account/jest.config.js @@ -4,8 +4,7 @@ module.exports = { ...baseConfigForPackages, preset: 'ts-jest', moduleNameMapper: { - '\\.css$': '/../../__mocks__/styleMock.js', - '\\.s(c|a)ss$': '/../../__mocks__/styleMock.js', + '\\.(s(c|a)ss|css|less)$': '/../../__mocks__/styleMock.js', '^.+\\.svg$': '/../../__mocks__/styleMock.js', '@deriv-com/translations': '/../../__mocks__/translation.mock.js', '^Assets/(.*)$': '/src/Assets/$1', diff --git a/packages/account/src/Configs/personal-details-config.ts b/packages/account/src/Configs/personal-details-config.ts index bf7c3c548bff..b6889e8df665 100644 --- a/packages/account/src/Configs/personal-details-config.ts +++ b/packages/account/src/Configs/personal-details-config.ts @@ -101,12 +101,9 @@ export const personal_details_config = ({ ['phone', localize('Phone is not in a proper format.')], [ (value: string) => { - // phone_trim uses regex that trims non-digits - const phone_trim = value.replace(/\D/g, ''); - // minimum characters required is 9 numbers (excluding +- signs or space) - return validLength(phone_trim, { min: PHONE_NUMBER_LENGTH.MIN, max: PHONE_NUMBER_LENGTH.MAX }); + return validLength(value, { min: PHONE_NUMBER_LENGTH.MIN, max: PHONE_NUMBER_LENGTH.MAX }); }, - localize('You should enter {{min}}-{{max}} numbers.', { + localize('You should enter {{min}}-{{max}} characters.', { min: PHONE_NUMBER_LENGTH.MIN, max: PHONE_NUMBER_LENGTH.MAX, }), diff --git a/packages/account/src/Constants/personal-details.ts b/packages/account/src/Constants/personal-details.ts index 936d07ecec71..465fdeb533cd 100644 --- a/packages/account/src/Constants/personal-details.ts +++ b/packages/account/src/Constants/personal-details.ts @@ -1,4 +1,4 @@ export const PHONE_NUMBER_LENGTH = { MIN: 9, - MAX: 35, + MAX: 20, }; diff --git a/packages/account/src/Constants/routes-config.tsx b/packages/account/src/Constants/routes-config.tsx index cae933c6c535..4f739697a4a8 100644 --- a/packages/account/src/Constants/routes-config.tsx +++ b/packages/account/src/Constants/routes-config.tsx @@ -26,6 +26,11 @@ const Passwords = makeLazyLoader( () => )(); +const PhoneVerificationPage = makeLazyLoader( + () => moduleLoader(() => import('../Sections/Profile/PhoneVerification')), + () => +)(); + const Passkeys = makeLazyLoader( () => moduleLoader(() => import('../Sections/Security/Passkeys')), () => @@ -109,6 +114,12 @@ const initRoutesConfig = () => [ getTitle: () => localize('Profile'), icon: 'IcUserOutline', subroutes: [ + { + path: routes.phone_verification, + component: PhoneVerificationPage, + getTitle: () => localize('Phone number verification'), + is_hidden: true, + }, { path: routes.personal_details, component: PersonalDetails, diff --git a/packages/account/src/Containers/__tests__/routes.spec.tsx b/packages/account/src/Containers/__tests__/routes.spec.tsx index 5d2ad2c67afe..076bd2c3a205 100644 --- a/packages/account/src/Containers/__tests__/routes.spec.tsx +++ b/packages/account/src/Containers/__tests__/routes.spec.tsx @@ -16,6 +16,19 @@ jest.mock('@deriv/components', () => ({ })); describe('', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); const history = createBrowserHistory(); const mock_root_store = mockStore({ diff --git a/packages/account/src/Containers/routes.tsx b/packages/account/src/Containers/routes.tsx index 63566793c9db..3dbb9079f1b2 100644 --- a/packages/account/src/Containers/routes.tsx +++ b/packages/account/src/Containers/routes.tsx @@ -1,18 +1,23 @@ -import React from 'react'; import { withRouter } from 'react-router'; import { observer, useStore } from '@deriv/stores'; import { BinaryRoutes } from '../Components/Routes'; import ErrorComponent from '../Components/error-component'; +import { ThemeProvider } from '@deriv-com/quill-ui'; const Routes = observer(() => { - const { client, common } = useStore(); + const { client, common, ui } = useStore(); const { is_logged_in, is_logging_in } = client; const { error, has_error } = common; + const { is_dark_mode_on } = ui; if (has_error) { return ; } - return ; + return ( + + + + ); }); // need to wrap withRouter diff --git a/packages/account/src/Helpers/utils.tsx b/packages/account/src/Helpers/utils.tsx index 4e41947ea623..1fea91cf93e5 100644 --- a/packages/account/src/Helpers/utils.tsx +++ b/packages/account/src/Helpers/utils.tsx @@ -10,6 +10,7 @@ import { getIDVNotApplicableOption, IDV_ERROR_STATUS, AUTH_STATUS_CODES, + VERIFICATION_SERVICES, } from '@deriv/shared'; import { localize } from '@deriv-com/translations'; import { getIDVDocuments } from '../Configs/idv-document-config'; @@ -277,3 +278,9 @@ export const verifyFields = (status: TIDVErrorStatus) => { export const isSpecialPaymentMethod = (payment_method_icon: string) => ['IcOnlineNaira', 'IcAstroPayLight', 'IcAstroPayDark'].some(icon => icon === payment_method_icon); + +export const convertPhoneTypeDisplay = (phone_verification_type: string) => { + if (phone_verification_type === VERIFICATION_SERVICES.SMS) return phone_verification_type.toUpperCase(); + + return 'WhatsApp'; +}; diff --git a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details-form.spec.tsx b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details-form.spec.tsx index 7f4652e8014c..8aa4b8541e11 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details-form.spec.tsx +++ b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details-form.spec.tsx @@ -6,7 +6,7 @@ import { APIProvider } from '@deriv/api'; import userEvent from '@testing-library/user-event'; import { StoreProvider, mockStore } from '@deriv/stores'; import PersonalDetailsForm from '../personal-details-form'; -import { useResidenceList } from '@deriv/hooks'; +import { useGrowthbookGetFeatureValue, useResidenceList } from '@deriv/hooks'; afterAll(cleanup); jest.mock('@deriv/components', () => ({ @@ -16,6 +16,7 @@ jest.mock('@deriv/components', () => ({ jest.mock('@deriv/shared/src/services/ws-methods', () => ({ WS: { + send: jest.fn().mockResolvedValue({ time: 1620000000 }), wait: (...payload: []) => Promise.resolve([...payload]), }, useWS: () => undefined, @@ -32,6 +33,7 @@ jest.mock('@deriv/hooks', () => ({ ...jest.requireActual('@deriv/hooks'), useStatesList: jest.fn(() => ({ data: residence_list, isLoading: false })), useResidenceList: jest.fn(() => ({ data: residence_list, isLoading: false })), + useGrowthbookGetFeatureValue: jest.fn(), })); describe('', () => { @@ -44,6 +46,9 @@ describe('', () => { place_of_birth: 'Thailand', citizen: 'Thailand', email_consent: 1, + phone_number_verification: { + verified: 0, + }, }, }, }); @@ -60,15 +65,15 @@ describe('', () => { ); }; + beforeEach(() => { + (useGrowthbookGetFeatureValue as jest.Mock).mockReturnValue([true]); + }); + it('should render successfully', async () => { renderComponent(); expect(screen.queryByText(/Loading/)).not.toBeInTheDocument(); await waitFor(() => { - expect( - screen.getByText( - /Please make sure your information is correct or it may affect your trading experience./i - ) - ).toBeInTheDocument(); + expect(screen.getByText(/Ensure your information is correct./i)).toBeInTheDocument(); }); }); @@ -176,14 +181,14 @@ describe('', () => { ).toBeInTheDocument(); }); - it('should update user profile after clicking on submit', () => { + it('should update user profile after clicking on Save changes', () => { renderComponent(); const first_name = screen.getByTestId('dt_first_name') as HTMLInputElement; expect(first_name.value).toBe('John'); userEvent.clear(first_name); userEvent.type(first_name, 'James'); - const submit_button = screen.getByRole('button', { name: /Submit/ }); - userEvent.click(submit_button); + const save_changes_button = screen.getByRole('button', { name: /Save changes/ }); + userEvent.click(save_changes_button); expect(first_name.value).toBe('James'); }); diff --git a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/verify-button.spec.tsx b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/verify-button.spec.tsx new file mode 100644 index 000000000000..6e3f5ff09539 --- /dev/null +++ b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/verify-button.spec.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { Router } from 'react-router'; +import { createBrowserHistory } from 'history'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { usePhoneNumberVerificationSetTimer, useVerifyEmail } from '@deriv/hooks'; +import { routes } from '@deriv/shared'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { VerifyButton } from '../verify-button'; +import { GetSettings, ResidenceList, StatesList } from '@deriv/api-types'; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + usePhoneNumberVerificationSetTimer: jest.fn(), + useVerifyEmail: jest.fn(() => ({ + sendPhoneNumberVerifyEmail: jest.fn(), + WS: {}, + error: null, + })), + useSettings: jest.fn(() => ({ + refetch: jest.fn(), + mutation: { + mutateAsync: jest.fn(() => Promise.resolve()), + isLoading: false, + }, + })), +})); + +const mockAccountSettings: GetSettings = { + immutable_fields: ['place_of_birth'], + place_of_birth: 'UK', + tax_residence: 'UK', + tax_identification_number: '12345', + account_opening_reason: 'Hedging', +}; + +const mockResidenceList: ResidenceList = [ + { value: 'UK', text: 'United Kingdom' }, + { value: 'US', text: 'United States' }, +]; + +const mockStatesList: StatesList = []; + +describe('VerifyButton', () => { + beforeEach(() => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_otp_request: '' }); + }); + const history = createBrowserHistory(); + const mock_store = mockStore({ + client: { + account_settings: { + phone_number_verification: { + verified: 0, + }, + }, + }, + }); + let mock_next_email_otp_request_timer = 0; + const mock_set_status = jest.fn(); + + const renderWithRouter = () => { + return render( + + + + + + ); + }; + + beforeEach(() => { + mock_next_email_otp_request_timer = 0; + }); + + it('should render Verify Button', () => { + renderWithRouter(); + expect(screen.getByText('Verify')).toBeInTheDocument(); + }); + + it('should redirect user to phone-verification page when clicked on Verify Button', () => { + (useVerifyEmail as jest.Mock).mockReturnValue({ + sendPhoneNumberVerifyEmail: jest.fn(), + WS: { + isSuccess: true, + }, + }); + renderWithRouter(); + const verifyButton = screen.getByText('Verify'); + userEvent.click(verifyButton); + expect(history.location.pathname).toBe(routes.phone_verification); + }); + + it('should setStatus with error returned by WS', () => { + (useVerifyEmail as jest.Mock).mockReturnValue({ + sendPhoneNumberVerifyEmail: jest.fn(), + WS: { + isSuccess: false, + }, + error: { + message: 'Phone Taken', + code: 'PhoneNumberTaken', + }, + }); + renderWithRouter(); + const verifyButton = screen.getByText('Verify'); + userEvent.click(verifyButton); + expect(mock_set_status).toBeCalledWith({ msg: 'Phone Taken', code: 'PhoneNumberTaken' }); + }); + + it('should render Verify Button with timer if next_otp_request has value', () => { + mock_next_email_otp_request_timer = 2; + renderWithRouter(); + expect(screen.getByText('Verify in 2s')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Verify in 2s' })).toBeDisabled(); + }); + + it('should render Verified text', () => { + if (mock_store.client.account_settings.phone_number_verification) + mock_store.client.account_settings.phone_number_verification.verified = 1; + renderWithRouter(); + expect(screen.getByText('Verified')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PersonalDetails/personal-details-form.tsx b/packages/account/src/Sections/Profile/PersonalDetails/personal-details-form.tsx index 8ac9321ec2f2..67ea9e2d0d8d 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/personal-details-form.tsx +++ b/packages/account/src/Sections/Profile/PersonalDetails/personal-details-form.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, Fragment } from 'react'; +import { useState, useRef, useEffect, Fragment, ChangeEvent } from 'react'; import clsx from 'clsx'; import { Formik, Form, FormikHelpers } from 'formik'; import { useHistory } from 'react-router'; @@ -11,6 +11,7 @@ import { HintBox, Input, Loading, + OpenLiveChatLink, SelectNative, Text, } from '@deriv/components'; @@ -29,8 +30,15 @@ import { getEmploymentStatusList } from 'Sections/Assessment/FinancialAssessment import InputGroup from './input-group'; import { getPersonalDetailsInitialValues, getPersonalDetailsValidationSchema, makeSettingsRequest } from './validation'; import FormSelectField from 'Components/forms/form-select-field'; +import { VerifyButton } from './verify-button'; import { useInvalidateQuery } from '@deriv/api'; -import { useStatesList, useResidenceList } from '@deriv/hooks'; +import { + useStatesList, + useResidenceList, + useGrowthbookGetFeatureValue, + usePhoneNumberVerificationSetTimer, + useIsPhoneNumberVerified, +} from '@deriv/hooks'; type TRestState = { show_form: boolean; @@ -44,12 +52,17 @@ const PersonalDetailsForm = observer(() => { const [is_submit_success, setIsSubmitSuccess] = useState(false); const invalidate = useInvalidateQuery(); const history = useHistory(); + const [isPhoneNumberVerificationEnabled] = useGrowthbookGetFeatureValue({ + featureFlag: 'phone_number_verification', + }); + const { next_email_otp_request_timer, is_email_otp_timer_loading } = usePhoneNumberVerificationSetTimer(); const { client, notifications, common: { is_language_changing }, } = useStore(); + const { is_phone_number_verified } = useIsPhoneNumberVerified(); const { account_settings, @@ -104,14 +117,35 @@ const PersonalDetailsForm = observer(() => { } }, [invalidate, is_language_changing]); + const hintMessage = () => { + if (isPhoneNumberVerificationEnabled) { + if (is_phone_number_verified) { + return ( + , + ]} + /> + ); + } + } else { + return null; + } + }; + const onSubmit = async (values: GetSettings, { setStatus, setSubmitting }: FormikHelpers) => { - setStatus({ msg: '' }); + setStatus({ msg: '', code: '' }); const request = makeSettingsRequest({ ...values }, residence_list, states_list, is_virtual); setIsBtnLoading(true); const data = await WS.authorized.setSettings(request); if (data.error) { - setStatus({ msg: data.error.message }); + setStatus({ msg: data.error.message, code: data.error.code }); setIsBtnLoading(false); setSubmitting(false); } else { @@ -189,6 +223,8 @@ const PersonalDetailsForm = observer(() => { const is_account_verified = is_poa_verified && is_poi_verified; + const stripped_phone_number = `+${account_settings.phone?.replace(/\D/g, '')}`; + //Generate Redirection Link to user based on verification status const getRedirectionLink = () => { if (!is_poi_verified) { @@ -199,6 +235,24 @@ const PersonalDetailsForm = observer(() => { return undefined; }; + const displayErrorMessage = (status: any) => { + if (status?.code === 'PhoneNumberTaken') { + return ( + ]} + /> + } + text_color='loss-danger' + weight='none' + /> + ); + } + return ; + }; + const PersonalDetailSchema = getPersonalDetailsValidationSchema(is_eu, is_virtual); const initialValues = getPersonalDetailsInitialValues(account_settings, residence_list, states_list, is_virtual); @@ -213,6 +267,7 @@ const PersonalDetailsForm = observer(() => { {({ values, errors, + setStatus, status, touched, handleChange, @@ -362,13 +417,38 @@ const PersonalDetailsForm = observer(() => { label={localize('Phone number*')} //@ts-expect-error type of residence should not be null: needs to be updated in GetSettings type value={values.phone} - onChange={handleChange} + hint={hintMessage()} + onChange={(e: ChangeEvent) => { + let phone_number = e.target.value.replace(/\D/g, ''); + phone_number = phone_number.length === 0 ? '+' : `+${phone_number}`; + setFieldValue('phone', phone_number, true); + setStatus(''); + }} onBlur={handleBlur} required error={errors.phone} - disabled={isFieldDisabled('phone')} + disabled={ + isFieldDisabled('phone') || + !!next_email_otp_request_timer || + is_email_otp_timer_loading + } data-testid='dt_phone' /> + {isPhoneNumberVerificationEnabled && ( + + )} )} @@ -642,7 +722,7 @@ const PersonalDetailsForm = observer(() => { - {status?.msg && } + {status?.msg && displayErrorMessage(status)} {!is_virtual && !(isSubmitting || is_submit_success || status?.msg) && ( { color='prominent' align={isDesktop ? 'right' : 'center'} > - {localize( - 'Please make sure your information is correct or it may affect your trading experience.' - )} + )} + + ); + } +); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/cancel-phone-verification-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/cancel-phone-verification-modal.spec.tsx new file mode 100644 index 000000000000..658f329eefbf --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/cancel-phone-verification-modal.spec.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CancelPhoneVerificationModal from '../cancel-phone-verification-modal'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { routes } from '@deriv/shared'; +import { useGrowthbookGetFeatureValue } from '@deriv/hooks'; + +const mock_push = jest.fn(); +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useHistory: () => ({ + push: mock_push, + block: jest.fn(callback => { + callback({ pathname: routes.personal_details }); + return jest.fn(); + }), + }), + useLocation: () => ({ + pathname: '/phone-verification', + }), +})); +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + usePhoneNumberVerificationSessionTimer: jest.fn(() => ({ + should_show_session_timeout_modal: false, + })), + useGrowthbookGetFeatureValue: jest.fn(), +})); + +describe('CancelPhoneVerificationModal', () => { + let modal_root_el: HTMLElement; + + beforeAll(() => { + (useGrowthbookGetFeatureValue as jest.Mock).mockReturnValue([true]); + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const mock_store = mockStore({}); + + const buttons = [/Continue verification/, /Cancel/]; + + const renderComponent = () => { + render( + + + + ); + }; + + it('it should render CancelPhoneVerificationModal', () => { + renderComponent(); + buttons.forEach(value => { + expect(screen.getByRole('button', { name: value })).toBeInTheDocument(); + }); + expect(screen.getByText(/Cancel phone number verification?/)).toBeInTheDocument(); + expect(screen.getByText(/If you cancel, you'll lose all progress./)).toBeInTheDocument(); + }); + + it('it should render only mockSetShowCancelModal when Continue verification is clicked', () => { + renderComponent(); + const cancelButton = screen.getByRole('button', { name: buttons[0] }); + userEvent.click(cancelButton); + expect(mock_push).not.toBeCalled(); + }); + + it('it should render mockSetShowCancelModal and mock_back_router when Cancel is clicked', () => { + renderComponent(); + const cancelButton = screen.getByRole('button', { name: buttons[1] }); + userEvent.click(cancelButton); + expect(mock_push).toBeCalledWith(routes.personal_details); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/confirm-phone-number.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/confirm-phone-number.spec.tsx new file mode 100644 index 000000000000..ba67ed481cd1 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/confirm-phone-number.spec.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { usePhoneNumberVerificationSetTimer, useRequestPhoneNumberOTP, useSettings } from '@deriv/hooks'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import ConfirmPhoneNumber from '../confirm-phone-number'; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useRequestPhoneNumberOTP: jest.fn(() => ({ + error_message: '', + requestOnWhatsApp: jest.fn(), + requestOnSMS: jest.fn(), + setErrorMessage: jest.fn(), + setUsersPhoneNumber: jest.fn(), + setIsDisabledRequestButton: jest.fn(), + })), + useSettings: jest.fn(() => ({ + data: {}, + invalidate: jest.fn(), + })), + usePhoneNumberVerificationSetTimer: jest.fn(() => ({ + next_phone_otp_request_timer: undefined, + is_phone_otp_timer_loading: false, + })), +})); + +describe('ConfirmPhoneNumber', () => { + const store = mockStore({ + ui: { + setShouldShowPhoneNumberOTP: jest.fn(), + }, + }); + + const mockSetOtp = jest.fn(); + const whatsapp_button_text = 'Get code via WhatsApp'; + const sms_button_text = 'Get code via SMS'; + + it('should render ConfirmPhoneNumber', () => { + (useSettings as jest.Mock).mockReturnValue({ + data: { phone: '+0123456789' }, + }); + render( + + + + ); + const phone_number_textfield = screen.getByRole('textbox', { name: 'Phone number' }); + expect(screen.getByText('Step 2 of 3: Confirm your phone number')).toBeInTheDocument(); + expect(phone_number_textfield).toBeInTheDocument(); + expect(phone_number_textfield).toHaveValue('0123456789'); + expect(screen.getByRole('button', { name: sms_button_text })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: whatsapp_button_text })).toBeInTheDocument(); + }); + + it('should call setErrorMessage when the user presses a key', async () => { + const mock_set_error_message = jest.fn(); + const mock_set_is_disabled_request_button = jest.fn(); + (useRequestPhoneNumberOTP as jest.Mock).mockReturnValue({ + setErrorMessage: mock_set_error_message, + setIsDisabledRequestButton: mock_set_is_disabled_request_button, + }); + render( + + + + ); + const phone_number_textfield = screen.getByRole('textbox', { name: 'Phone number' }); + expect(screen.getByText('Step 2 of 3: Confirm your phone number')).toBeInTheDocument(); + expect(phone_number_textfield).toBeInTheDocument(); + userEvent.clear(phone_number_textfield); + userEvent.type(phone_number_textfield, '+01293291291'); + await waitFor(() => { + expect(mock_set_error_message).toHaveBeenCalled(); + expect(mock_set_is_disabled_request_button).toHaveBeenCalled(); + }); + }); + + it('should display given error message', () => { + (useRequestPhoneNumberOTP as jest.Mock).mockReturnValue({ + error_message: 'This is an error message', + }); + render( + + + + ); + expect(screen.getByText(/This is an error message/)).toBeInTheDocument(); + }); + + it('should render handleError function when WS returns error promises', async () => { + const mock_handle_error = jest.fn().mockResolvedValue({ error: null }); + (useRequestPhoneNumberOTP as jest.Mock).mockReturnValue({ + requestOnWhatsApp: jest.fn(), + setUsersPhoneNumber: mock_handle_error, + }); + + render( + + + + ); + const whatsapp_btn = screen.getByRole('button', { name: whatsapp_button_text }); + userEvent.click(whatsapp_btn); + await waitFor(() => { + expect(mock_handle_error).toBeCalledTimes(1); + }); + }); + + it('should call requestOnWhatsApp when Whatsapp button is clicked', async () => { + const mockWhatsappButtonClick = jest.fn(); + + (useRequestPhoneNumberOTP as jest.Mock).mockReturnValue({ + requestOnWhatsApp: mockWhatsappButtonClick, + setUsersPhoneNumber: jest.fn().mockResolvedValue({ error: null }), + }); + render( + + + + ); + const whatsapp_btn = screen.getByRole('button', { name: whatsapp_button_text }); + userEvent.click(whatsapp_btn); + await waitFor(() => { + expect(mockWhatsappButtonClick).toHaveBeenCalled(); + }); + }); + + it('should call requestOnSMS when SMS button is clicked', async () => { + const mockSmsButtonClick = jest.fn(); + + (useRequestPhoneNumberOTP as jest.Mock).mockReturnValue({ + requestOnSMS: mockSmsButtonClick, + setUsersPhoneNumber: jest.fn().mockResolvedValue({ error: null }), + }); + render( + + + + ); + const sms_btn = screen.getByRole('button', { name: sms_button_text }); + userEvent.click(sms_btn); + await waitFor(() => { + expect(mockSmsButtonClick).toHaveBeenCalled(); + }); + }); + + it('should make both buttons disabled if next_otp_request text is provided', async () => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_phone_otp_request_timer: 60 }); + render( + + + + ); + const sms_btn = screen.getByRole('button', { name: sms_button_text }); + const whatsapp_btn = screen.getByRole('button', { name: whatsapp_button_text }); + expect(sms_btn).toBeDisabled(); + expect(whatsapp_btn).toBeDisabled(); + }); + + it('should get snackbar text when next_otp_request text is provided', async () => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_phone_otp_request_timer: 60 }); + render( + + + + ); + expect(screen.getByText(/Request new code in 1 minute./)); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/cool-down-period-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/cool-down-period-modal.spec.tsx new file mode 100644 index 000000000000..b3ebaa96daf6 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/cool-down-period-modal.spec.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import CoolDownPeriodModal from '../cool-down-period-modal'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { routes } from '@deriv/shared'; + +const mock_push_function = jest.fn(); +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useHistory: () => ({ + push: mock_push_function, + }), +})); + +describe('CooldownPeriodModal', () => { + const mock_store = mockStore({}); + const show_cool_down_period_modal = true; + const mockSetShowCoolDownPeriodModal = jest.fn(); + + const renderComponent = () => { + render( + + + + ); + }; + it('should show CooldownPeriodModal when show_cool_down_period_modal is true', () => { + renderComponent(); + expect(screen.getByText(/OTP limit reached/)).toBeInTheDocument(); + expect(screen.getByText(/Request a new OTP after 10 minutes./)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /OK/ })).toBeInTheDocument(); + }); + + it('should call history.push, setIsForcedToExitPnv, mockSetShowCoolDownPeriodModal with value of personal details', () => { + renderComponent(); + const ok_button = screen.getByRole('button', { name: /OK/ }); + userEvent.click(ok_button); + expect(mock_push_function).toBeCalledWith(routes.personal_details); + expect(mockSetShowCoolDownPeriodModal).toBeCalledWith(false); + expect(mock_store.ui.setIsForcedToExitPnv).toBeCalledWith(false); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/didnt-get-the-code-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/didnt-get-the-code-modal.spec.tsx new file mode 100644 index 000000000000..e00213ede6dd --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/didnt-get-the-code-modal.spec.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import DidntGetTheCodeModal from '../didnt-get-the-code-modal'; +import userEvent from '@testing-library/user-event'; +import { VERIFICATION_SERVICES } from '@deriv/shared'; + +describe('DidntGetTheCodeModal', () => { + const mockClearOtpValue = jest.fn(); + const mockSetShouldShowDidntGetTheCodeModal = jest.fn(); + const mockSetOtpVerification = jest.fn(); + const mockReInitializeGetSettings = jest.fn(); + const mockSetIsButtonDisabled = jest.fn(); + const mockRequestOnWhatsApp = jest.fn(); + const mockRequestOnSms = jest.fn(); + const resend_code_text = /Resend code/; + + const renderComponent = (phone_verification_type: string) => { + render( + + ); + }; + + beforeEach(() => { + mockSetShouldShowDidntGetTheCodeModal.mockClear(); + mockSetOtpVerification.mockClear(); + mockRequestOnSms.mockClear(); + mockRequestOnWhatsApp.mockClear(); + }); + + it('should render DidntGetTheCodeModal', () => { + renderComponent(VERIFICATION_SERVICES.SMS); + expect(screen.getByText(/Didn't receive a code/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: resend_code_text })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Send code via WhatsApp/ })).toBeInTheDocument(); + }); + + it('should show Send code via SMS if phone_verification_type is whatsapp', () => { + renderComponent(VERIFICATION_SERVICES.WHATSAPP); + expect(screen.getByRole('button', { name: /Send code via SMS/ })).toBeInTheDocument(); + }); + + it('should render setShouldShowDidintGetTheCodeModal when Resend code is clicked', () => { + renderComponent(VERIFICATION_SERVICES.SMS); + const resend_code_button = screen.getByRole('button', { name: resend_code_text }); + userEvent.click(resend_code_button); + expect(mockSetShouldShowDidntGetTheCodeModal).toHaveBeenCalledTimes(1); + }); + + it('should render mockRequestOnSMS and setOtpVerification with phone_verification_type: sms when Resend code is clicked, phone_verification_type is sms', () => { + renderComponent(VERIFICATION_SERVICES.SMS); + const resend_code_button = screen.getByRole('button', { name: resend_code_text }); + userEvent.click(resend_code_button); + expect(mockRequestOnSms).toHaveBeenCalledTimes(1); + expect(mockSetOtpVerification).toBeCalledWith({ + show_otp_verification: true, + phone_verification_type: VERIFICATION_SERVICES.SMS, + }); + }); + + it('should render mockRequestOnWhatsapp and setOtpVerification with phone_verification_type: whatsapp when Resend code is clicked, phone_verification_type is whatsapp', () => { + renderComponent(VERIFICATION_SERVICES.WHATSAPP); + const resend_code_button = screen.getByRole('button', { name: resend_code_text }); + userEvent.click(resend_code_button); + expect(mockRequestOnWhatsApp).toHaveBeenCalledTimes(1); + expect(mockSetOtpVerification).toBeCalledWith({ + show_otp_verification: true, + phone_verification_type: VERIFICATION_SERVICES.WHATSAPP, + }); + }); + + it('should render mockRequestOnSMS and setOtpVerification with phone_verification_type: sms when Send code via SMS is clicked, phone_verification_type is whatsapp', () => { + renderComponent(VERIFICATION_SERVICES.WHATSAPP); + const resend_code_button = screen.getByRole('button', { name: /Send code via SMS/ }); + userEvent.click(resend_code_button); + expect(mockRequestOnSms).toHaveBeenCalledTimes(1); + expect(mockSetOtpVerification).toBeCalledWith({ + show_otp_verification: true, + phone_verification_type: VERIFICATION_SERVICES.SMS, + }); + }); + + it('should render mockRequestOnWhatsApp and setOtpVerification with phone_verification_type: whatsapp when Send code via WhatsApp is clicked, phone_verification_type is whatsapp', () => { + renderComponent(VERIFICATION_SERVICES.SMS); + const resend_code_button = screen.getByRole('button', { name: /Send code via WhatsApp/ }); + userEvent.click(resend_code_button); + expect(mockRequestOnWhatsApp).toHaveBeenCalledTimes(1); + expect(mockSetOtpVerification).toBeCalledWith({ + show_otp_verification: true, + phone_verification_type: VERIFICATION_SERVICES.WHATSAPP, + }); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/otp-verification.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/otp-verification.spec.tsx new file mode 100644 index 000000000000..f3f371366e7b --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/otp-verification.spec.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import OTPVerification from '../otp-verification'; +import { useSendOTPVerificationCode, useSettings } from '@deriv/hooks'; +import userEvent from '@testing-library/user-event'; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useSettings: jest.fn(), + useSendOTPVerificationCode: jest.fn(), + usePhoneNumberVerificationSetTimer: jest.fn(() => ({ + setNextEmailOtpRequestTimer: jest.fn(), + setNextPhoneOtpRequestTimer: jest.fn(), + is_email_otp_timer_loading: false, + is_phone_otp_timer_loading: false, + })), +})); + +jest.mock('../phone-number-verified-modal', () => jest.fn(() =>
Phone Number Verified Modal
)); +jest.mock('../resend-code-timer', () => jest.fn(() =>
Resend Code Timer
)); +jest.mock('../cool-down-period-modal', () => jest.fn(() =>
Cooldown Period Modal
)); + +describe('OTPVerification', () => { + const store = mockStore({ + client: { + email: 'johndoe@regentmarkets.com', + }, + ui: { + should_show_phone_number_otp: false, + }, + }); + let phone_verification_type = 'sms'; + const mockSetOtpVerification = jest.fn(); + const mockSendPhoneOTPVerification = jest.fn(); + const mockSetPhoneOtpErrorMessage = jest.fn(); + const renderComponent = () => { + render( + + + + ); + }; + + beforeEach(() => { + (useSettings as jest.Mock).mockReturnValue({ + data: { + email: 'johndoe@regentmarkets.com', + }, + invalidate: jest.fn(() => Promise.resolve()), + }); + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: jest.fn(), + }); + }); + + it('should render ConfirmYourEmail in OTP Verification', () => { + renderComponent(); + expect(screen.getByText('Step 1 of 3: Email verification needed')).toBeInTheDocument(); + expect(screen.getByText(/We've sent a verification code to/)).toBeInTheDocument(); + expect(screen.getByText('johndoe@regentmarkets.com')).toBeInTheDocument(); + expect(screen.getByText(/Enter the code below to verify it's you/)).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /Verification code/ })).toBeInTheDocument(); + expect(screen.getByText(/Resend Code Timer/)).toBeInTheDocument(); + }); + + it('should render Verify your number in OTP Verification', () => { + store.ui.should_show_phone_number_otp = true; + renderComponent(); + expect(screen.getByText('Step 3 of 3: Verify your number')).toBeInTheDocument(); + expect(screen.getByText(/Enter the 6-digit code sent to you via SMS at ./)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Edit number/ })).toBeInTheDocument(); + }); + + it('should render whatsapp when phone_verification_type is whatsapp', () => { + store.ui.should_show_phone_number_otp = true; + phone_verification_type = 'whatsapp'; + renderComponent(); + expect(screen.getByText(/WhatsApp/)).toBeInTheDocument(); + }); + + it('should not enabled Verify button when otp does not have 6 characters', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: jest.fn(), + setPhoneOtpErrorMessage: jest.fn(), + }); + renderComponent(); + const otp_textfield = screen.getByRole('textbox'); + const verify_button = screen.getByRole('button', { name: 'Verify' }); + userEvent.type(otp_textfield, '12345'); + expect(verify_button).toBeDisabled(); + }); + + it('should contain value of 123456 for otp textfield component', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: jest.fn(), + setPhoneOtpErrorMessage: jest.fn(), + }); + renderComponent(); + const otp_textfield = screen.getByRole('textbox'); + userEvent.type(otp_textfield, '123456'); + expect(otp_textfield).toHaveValue('123456'); + }); + + it('should render mockSendPhoneOTPVerification when Verify button is clicked', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: mockSendPhoneOTPVerification, + setPhoneOtpErrorMessage: jest.fn(), + }); + renderComponent(); + const otp_textfield = screen.getByRole('textbox'); + const verify_button = screen.getByRole('button', { name: 'Verify' }); + userEvent.type(otp_textfield, '123456'); + expect(verify_button).toBeEnabled(); + userEvent.click(verify_button); + expect(mockSendPhoneOTPVerification).toBeCalledTimes(1); + }); + + it('should show error message when API returns error', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: mockSendPhoneOTPVerification, + phone_otp_error_message: 'Error Message', + }); + renderComponent(); + expect(screen.getByText(/Error Message/)).toBeInTheDocument(); + }); + + it('should render mockSetPhoneOtpErrorMessage to be empty when users retype inside textfield', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: mockSendPhoneOTPVerification, + phone_otp_error_message: 'Error Message', + setPhoneOtpErrorMessage: mockSetPhoneOtpErrorMessage, + }); + renderComponent(); + expect(screen.getByText(/Error Message/)).toBeInTheDocument(); + const otp_textfield = screen.getByRole('textbox'); + userEvent.type(otp_textfield, '123456'); + expect(mockSetPhoneOtpErrorMessage).toBeCalled(); + }); + + it('should display Phone Number Verified Modal when API returns phone_number_verified is true', () => { + store.ui.should_show_phone_number_otp = true; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendPhoneOTPVerification: mockSendPhoneOTPVerification, + is_phone_number_verified: true, + }); + renderComponent(); + expect(screen.getByText(/Phone Number Verified Modal/)).toBeInTheDocument(); + }); + + it('should render sendEmailOTPVerification when should_show_phone_number_otp is false', () => { + const mockSendEmailOTPVerification = jest.fn(); + store.ui.should_show_phone_number_otp = false; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendEmailOTPVerification: mockSendEmailOTPVerification, + setPhoneOtpErrorMessage: jest.fn(), + }); + renderComponent(); + const otp_textfield = screen.getByRole('textbox'); + const verify_button = screen.getByRole('button', { name: 'Verify' }); + userEvent.type(otp_textfield, '123456'); + expect(verify_button).toBeEnabled(); + userEvent.click(verify_button); + expect(mockSendEmailOTPVerification).toBeCalledTimes(1); + }); + + it('should render setOtpVerification and setVerificationCode when is_email_verified is true', () => { + store.ui.should_show_phone_number_otp = false; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + is_email_verified: true, + sendEmailOTPVerification: jest.fn(), + setPhoneOtpErrorMessage: jest.fn(), + }); + renderComponent(); + const otp_textfield = screen.getByRole('textbox'); + const verify_button = screen.getByRole('button', { name: 'Verify' }); + userEvent.type(otp_textfield, '123456'); + expect(verify_button).toBeEnabled(); + userEvent.click(verify_button); + expect(store.client.setVerificationCode).toBeCalled(); + expect(mockSetOtpVerification).toBeCalledWith({ phone_verification_type: '', show_otp_verification: false }); + }); + + it('should display cooldown period modal when show_cool_down_period_modal is true', () => { + store.ui.should_show_phone_number_otp = false; + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + show_cool_down_period_modal: true, + }); + renderComponent(); + expect(screen.getByText(/Cooldown Period Modal/)).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-number-verified-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-number-verified-modal.spec.tsx new file mode 100644 index 000000000000..a1eb61dcc832 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-number-verified-modal.spec.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { render, screen } from '@testing-library/react'; +import PhoneNumberVerifiedModal from '../phone-number-verified-modal'; +import userEvent from '@testing-library/user-event'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import { routes } from '@deriv/shared'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), +})); + +const mockPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockPush, + }), +})); + +describe('PhoneNumberVerifiedModal', () => { + let modal_root_el: HTMLElement; + const mock_store = mockStore({}); + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const renderModal = () => { + render( + + + + + + ); + }; + + it('it should render PhoneNumberVerifiedModal', () => { + renderModal(); + expect(screen.getByText(/Phone number verified/)).toBeInTheDocument(); + expect(screen.getByText(/is verified as your phone number./)).toBeInTheDocument(); + }); + + it('it should refetch GetSettings when done is clicked', async () => { + renderModal(); + const doneButton = screen.getByRole('button', { name: /OK/ }); + await userEvent.click(doneButton); + expect(mockPush).toHaveBeenCalledTimes(1); + expect(mockPush).toBeCalledWith(routes.traders_hub); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-card.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-card.spec.tsx new file mode 100644 index 000000000000..8d3c3b072671 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-card.spec.tsx @@ -0,0 +1,44 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import PhoneVerificationCard from '../phone-verification-card'; +import { usePhoneNumberVerificationSessionTimer } from '@deriv/hooks'; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + usePhoneNumberVerificationSessionTimer: jest.fn(() => ({ + formatted_time: '00:00', + })), +})); + +describe('ConfirmPhoneNumber', () => { + beforeEach(() => { + (usePhoneNumberVerificationSessionTimer as jest.Mock).mockReturnValue({ formatted_time: '00:00' }); + }); + + it('should render ConfirmPhoneNumber', () => { + render(Card Content); + expect(screen.getByText(/Card Content/)).toBeInTheDocument(); + expect(screen.getByText(/Card Content/)).not.toHaveClass( + 'phone-verification__card phone-verification__card--small-card' + ); + }); + + it('should include --small-card className if props is being passed in', () => { + render(Card Content); + const card_content = screen.getByText(/Card Content/); + expect(card_content).toHaveClass('phone-verification__card phone-verification__card--small-card'); + }); + + it('should have timer rendered in phone verification card', () => { + render(Card Content); + const time_remaining_component = screen.getByText(/Time remaining: 00:00/); + expect(time_remaining_component).toBeInTheDocument(); + }); + + it('should adjust timer value based on formatted_time return by hooks', () => { + (usePhoneNumberVerificationSessionTimer as jest.Mock).mockReturnValue({ formatted_time: '00:03' }); + render(Card Content); + const time_remaining_component = screen.getByText(/Time remaining: 00:03/); + expect(time_remaining_component).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-page.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-page.spec.tsx new file mode 100644 index 000000000000..dbc863d2f277 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/phone-verification-page.spec.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PhoneVerificationPage from '../phone-verification-page'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { useGrowthbookGetFeatureValue, useSendOTPVerificationCode } from '@deriv/hooks'; + +jest.mock('../otp-verification.tsx', () => jest.fn(() =>
Confirm Your Email
)); +jest.mock('../confirm-phone-number.tsx', () => jest.fn(() =>
Confirm Phone Number
)); +jest.mock('../cancel-phone-verification-modal', () => jest.fn(() =>
Cancel Phone Verification Modal
)); +jest.mock('../verification-link-expired-modal', () => jest.fn(() =>
Verification Link Expired Modal
)); +jest.mock('../session-timeout-modal.tsx', () => jest.fn(() =>
Session Timeout Modal
)); +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useSendOTPVerificationCode: jest.fn(() => ({ + email_otp_error: undefined, + is_email_verified: false, + sendEmailOTPVerification: jest.fn(), + })), + useGrowthbookGetFeatureValue: jest.fn(), + usePhoneNumberVerificationSessionTimer: jest.fn(() => ({ + formatted_time: '00:00', + })), +})); +jest.mock('@deriv/components', () => ({ + ...jest.requireActual('@deriv/components'), + Loading: jest.fn(() => 'mockedLoading'), +})); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn(), + }), +})); + +describe('ConfirmPhoneNumber', () => { + let mock_store_data = mockStore({}); + const renderComponent = () => { + render( + + + + ); + }; + beforeEach(() => { + (useGrowthbookGetFeatureValue as jest.Mock).mockReturnValue([true, true]); + mock_store_data = mockStore({ + client: { + verification_code: { + phone_number_verification: '', + }, + }, + ui: { + is_redirected_from_email: false, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render ConfirmPhoneNumber', () => { + renderComponent(); + expect(screen.getByText(/Back to personal details/)).toBeInTheDocument(); + expect(screen.getByText(/Confirm Your Email/)).toBeInTheDocument(); + }); + + it('should display cancel phone verification modal when back button is clicked', () => { + renderComponent(); + const backButton = screen.getByTestId('dt_phone_verification_back_btn'); + userEvent.click(backButton); + expect(screen.getByText(/Cancel Phone Verification Modal/)).toBeInTheDocument(); + }); + + it('should display mockedLoading and render sendEmailOTPVerification when phone_number_verification has value', () => { + const mockSendEmailOTPVerification = jest.fn(); + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + sendEmailOTPVerification: mockSendEmailOTPVerification, + }); + mock_store_data.client.verification_code.phone_number_verification = '123456'; + mock_store_data.client.is_authorize = true; + mock_store_data.ui.is_redirected_from_email = true; + renderComponent(); + expect(screen.getByText(/mockedLoading/)).toBeInTheDocument(); + expect(mockSendEmailOTPVerification).toBeCalledTimes(1); + }); + + it('should display Verification Link Expired Modal when hook returns error', async () => { + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + email_otp_error: { code: 'InvalidToken', message: '' }, + sendEmailOTPVerification: jest.fn(), + }); + renderComponent(); + expect(screen.getByText(/Verification Link Expired Modal/)).toBeInTheDocument(); + }); + + it('should display Confirm Phone Number when is_email_verified is true', async () => { + (useSendOTPVerificationCode as jest.Mock).mockReturnValue({ + is_email_verified: true, + }); + mock_store_data.ui.is_redirected_from_email = true; + renderComponent(); + expect(screen.getByText(/Confirm Phone Number/)).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/resend-code-timer.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/resend-code-timer.spec.tsx new file mode 100644 index 000000000000..50de56ab2c04 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/resend-code-timer.spec.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { usePhoneNumberVerificationSetTimer, useVerifyEmail } from '@deriv/hooks'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import ResendCodeTimer from '../resend-code-timer'; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useVerifyEmail: jest.fn(() => ({ + sendPhoneNumberVerifyEmail: jest.fn(), + WS: { + isSuccess: false, + }, + })), + useSettings: jest.fn(() => ({ + data: { + phone_number_verification: { + next_email_attempt: null, + next_attempt: null, + }, + }, + })), + usePhoneNumberVerificationSetTimer: jest.fn(() => ({ + next_phone_otp_request_timer: '', + next_email_otp_request_timer: '', + })), +})); + +describe('ConfirmPhoneNumber', () => { + beforeEach(() => { + (useVerifyEmail as jest.Mock).mockReturnValue({ + sendPhoneNumberVerifyEmail: jest.fn(), + WS: { isSuccess: false }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ + next_phone_otp_request_timer: '', + next_email_otp_request_timer: '', + }); + }); + + const mockSetShouldShowDidntGetTheCodeModal = jest.fn(); + const mockSetIsButtonDisabled = jest.fn(); + const mockReInitializeGetSettings = jest.fn(); + const mockClearOtpValue = jest.fn(); + + const renderComponent = (is_button_disabled = false, should_show_resend_code_button = true) => { + render( + + + + ); + }; + + const mock_store = mockStore({}); + it('should enable button if usePhoneNumberVerificationSetTimer did not return next_otp_request', async () => { + renderComponent(); + + expect(screen.queryByRole('button', { name: 'Resend code' })).toBeEnabled; + }); + + it('should disable button if usePhoneNumberVerificationSetTimer returns next_otp_request', async () => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_email_otp_request_timer: 59 }); + renderComponent(); + + expect(screen.queryByRole('button', { name: 'Resend code in 59s' })).toBeDisabled; + }); + + it('should trigger mockSend when send button is clicked', () => { + const mockSend = jest.fn(); + (useVerifyEmail as jest.Mock).mockReturnValue({ + sendPhoneNumberVerifyEmail: mockSend, + WS: { isSuccess: false }, + }); + renderComponent(); + const resend_button = screen.getByRole('button', { name: 'Resend code' }); + expect(resend_button).toBeEnabled(); + + userEvent.click(resend_button); + expect(mockSend).toBeCalled(); + }); + + it('should display Didn’t get the code? should_show_resend_code_button is false', () => { + renderComponent(false, false); + const resend_button = screen.getByRole('button', { name: "Didn't get the code?" }); + expect(resend_button).toBeInTheDocument(); + }); + + it('should display Didn’t get the code? (60s) when usePhoneNumberSetTimer returns (60s)', () => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_phone_otp_request_timer: 60 }); + renderComponent(false, false); + const resend_button = screen.getByRole('button', { name: "Didn't get the code? (1m)" }); + expect(resend_button).toBeInTheDocument(); + }); + + it('should trigger setShouldShowDidntGetTheCodeModal when Didn`t get the code is clicked', () => { + renderComponent(false, false); + const resend_button_after = screen.getByRole('button', { name: "Didn't get the code?" }); + userEvent.click(resend_button_after); + expect(mockSetShouldShowDidntGetTheCodeModal).toHaveBeenCalled(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/session-timeout-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/session-timeout-modal.spec.tsx new file mode 100644 index 000000000000..e03c83957722 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/session-timeout-modal.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import SessionTimeoutModal from '../session-timeout-modal'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import userEvent from '@testing-library/user-event'; +import { routes } from '@deriv/shared'; + +const mock_push = jest.fn(); + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + usePhoneNumberVerificationSessionTimer: jest.fn(() => ({ + should_show_session_timeout_modal: true, + })), +})); + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mock_push, + }), +})); + +describe('SessionTimeoutModal', () => { + const mock_store = mockStore({}); + const renderComponent = () => { + render( + + + + ); + }; + + it('should show SessionTimeoutModal content', () => { + renderComponent(); + expect(screen.getByText(/Session expired/)).toBeInTheDocument(); + expect(screen.getByText(/Restart your phone number verification./)).toBeInTheDocument(); + const ok_button = screen.getByRole('button', { name: 'OK' }); + expect(ok_button).toBeInTheDocument(); + }); + + it('should redirect user back to Personal Details Settings on OK button', () => { + renderComponent(); + const ok_button = screen.getByRole('button', { name: 'OK' }); + userEvent.click(ok_button); + expect(mock_push).lastCalledWith(routes.personal_details); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/validation.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/validation.spec.tsx new file mode 100644 index 000000000000..0b51dc7b682d --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/validation.spec.tsx @@ -0,0 +1,57 @@ +import { act } from '@testing-library/react'; +import { validatePhoneNumber } from '../validation'; + +describe('validatePhoneNumber', () => { + const setErrorMessage = jest.fn(); + const setIsDisabledRequestButton = jest.fn(); + + const error_message = 'Enter a valid phone number.'; + + it('should set an empty error message for a valid phone number', async () => { + const validPhoneNumber = '+1234567890'; + await act(async () => { + validatePhoneNumber(validPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith(''); + }); + + it('should set an error message for an invalid phone number', async () => { + const invalidPhoneNumber = 'invalid'; + await act(async () => { + validatePhoneNumber(invalidPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith([error_message]); + }); + + it('should set an error message for an empty phone number', async () => { + const invalidPhoneNumber = ''; + await act(async () => { + validatePhoneNumber(invalidPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith([error_message]); + }); + + it('should set an error message for an phone number more than 36 characters', async () => { + const invalidPhoneNumber = '+123123123123123123123123232333333333'; + await act(async () => { + validatePhoneNumber(invalidPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith([error_message]); + }); + + it('should set an error message for an phone number less than 8 characters', async () => { + const invalidPhoneNumber = '+1234567'; + await act(async () => { + validatePhoneNumber(invalidPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith([error_message]); + }); + + it('should set an error message for phone number without including + sign', async () => { + const invalidPhoneNumber = '0123456789'; + await act(async () => { + validatePhoneNumber(invalidPhoneNumber, setErrorMessage, setIsDisabledRequestButton); + }); + expect(setErrorMessage).toHaveBeenCalledWith([error_message]); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/__test__/verification-link-expired-modal.spec.tsx b/packages/account/src/Sections/Profile/PhoneVerification/__test__/verification-link-expired-modal.spec.tsx new file mode 100644 index 000000000000..92d903921eff --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/__test__/verification-link-expired-modal.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { APIProvider } from '@deriv/api'; +import { usePhoneNumberVerificationSetTimer } from '@deriv/hooks'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { routes } from '@deriv/shared'; +import VerificationLinkExpiredModal from '../verification-link-expired-modal'; + +const mock_push_function = jest.fn(); +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useHistory: () => ({ + push: mock_push_function, + }), +})); + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + usePhoneNumberVerificationSetTimer: jest.fn(() => ({ + next_email_otp_request_timer: '', + })), +})); + +describe('VerificationLinkExpiredModal', () => { + let modal_root_el: HTMLElement; + const mockSetShowVerificationLinkExpiredModal = jest.fn(); + + beforeEach(() => { + mockSetShowVerificationLinkExpiredModal.mockClear(); + mock_push_function.mockClear(); + }); + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const mock_store = mockStore({}); + + const buttons = [/Send new link/, /Cancel/]; + + const renderComponent = () => { + render( + + + + + + ); + }; + + it('should render VerificationLinkExpiredModal', () => { + renderComponent(); + buttons.forEach(value => { + expect(screen.getByRole('button', { name: value })).toBeInTheDocument(); + }); + expect(screen.getByText(/Link expired/)).toBeInTheDocument(); + expect(screen.getByText(/Request a new verification link via email./)).toBeInTheDocument(); + }); + + it('should render mockSetShowVerificationLinkExpiredModal and mock_back_router when Cancel is clicked', () => { + renderComponent(); + const cancelButton = screen.getByRole('button', { name: buttons[1] }); + userEvent.click(cancelButton); + expect(mockSetShowVerificationLinkExpiredModal).toBeCalledTimes(1); + expect(mock_push_function).toBeCalledWith(routes.personal_details); + }); + + it('should show in 60s which is coming from usePhoneNumberVerificationSetTimer', () => { + (usePhoneNumberVerificationSetTimer as jest.Mock).mockReturnValue({ next_email_otp_request_timer: 60 }); + renderComponent(); + expect(screen.getByText(/in 1m/)).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Profile/PhoneVerification/cancel-phone-verification-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/cancel-phone-verification-modal.tsx new file mode 100644 index 000000000000..c3b336b19deb --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/cancel-phone-verification-modal.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router'; +import { observer, useStore } from '@deriv/stores'; +import { useGrowthbookGetFeatureValue, useIsPhoneNumberVerified, usePhoneVerificationAnalytics } from '@deriv/hooks'; +import { Modal, Text } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; +import { useDevice } from '@deriv-com/ui'; + +const CancelPhoneVerificationModal = observer(() => { + const history = useHistory(); + const location = useLocation(); + const [show_modal, setShowModal] = useState(false); + const [next_location, setNextLocation] = useState(location.pathname); + const { ui, client } = useStore(); + const { setShouldShowPhoneNumberOTP, is_forced_to_exit_pnv } = ui; + const { setVerificationCode, is_virtual } = client; + const { isMobile } = useDevice(); + const { trackPhoneVerificationEvents } = usePhoneVerificationAnalytics(); + const { is_phone_number_verified } = useIsPhoneNumberVerified(); + const [isPhoneNumberVerificationEnabled] = useGrowthbookGetFeatureValue({ + featureFlag: 'phone_number_verification', + }); + + useEffect(() => { + const unblock = history.block((location: Location) => { + if ( + !show_modal && + !is_virtual && + !is_forced_to_exit_pnv && + !is_phone_number_verified && + isPhoneNumberVerificationEnabled + ) { + setShowModal(true); + setNextLocation(location.pathname); + return false; + } + return true; + }); + + return () => unblock(); + }, [ + history, + show_modal, + is_virtual, + is_forced_to_exit_pnv, + is_phone_number_verified, + isPhoneNumberVerificationEnabled, + ]); + + const handleStayAtPhoneVerificationPage = () => { + setShowModal(false); + setNextLocation(location.pathname); + }; + + const handleLeavePhoneVerificationPage = () => { + if (next_location) { + setVerificationCode('', 'phone_number_verification'); + setShouldShowPhoneNumberOTP(false); + setShowModal(false); + trackPhoneVerificationEvents({ + action: 'back', + }); + history.push(next_location); + } + }; + + return ( + } + buttonColor='coral' + showSecondaryButton + showCrossIcon + toggleModal={handleStayAtPhoneVerificationPage} + secondaryButtonLabel={} + secondaryButtonCallback={handleLeavePhoneVerificationPage} + > + } /> + +
+ + + +
+
+
+ ); +}); + +export default CancelPhoneVerificationModal; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/confirm-phone-number.tsx b/packages/account/src/Sections/Profile/PhoneVerification/confirm-phone-number.tsx new file mode 100644 index 000000000000..99fb1be02e61 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/confirm-phone-number.tsx @@ -0,0 +1,179 @@ +import { useState, useEffect, ChangeEvent } from 'react'; +import { + usePhoneNumberVerificationSetTimer, + usePhoneVerificationAnalytics, + useRequestPhoneNumberOTP, + useSettings, +} from '@deriv/hooks'; +import { VERIFICATION_SERVICES } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Button, Snackbar, Text, TextFieldAddon } from '@deriv-com/quill-ui'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import PhoneVerificationCard from './phone-verification-card'; +import { validatePhoneNumber } from './validation'; + +type TConfirmPhoneNumber = { + show_confirm_phone_number?: boolean; + setOtpVerification: (value: { show_otp_verification: boolean; phone_verification_type: string }) => void; +}; + +const ConfirmPhoneNumber = observer(({ show_confirm_phone_number, setOtpVerification }: TConfirmPhoneNumber) => { + const [phone_number, setPhoneNumber] = useState(''); + const [phone_verification_type, setPhoneVerificationType] = useState(''); + const [is_button_loading, setIsButtonLoading] = useState(false); + const { + requestOnSMS, + requestOnWhatsApp, + error_message, + setErrorMessage, + setUsersPhoneNumber, + is_email_verified, + email_otp_error, + is_disabled_request_button, + setIsDisabledRequestButton, + } = useRequestPhoneNumberOTP(); + const { data: account_settings, invalidate } = useSettings(); + const { ui } = useStore(); + const { setShouldShowPhoneNumberOTP } = ui; + const { next_phone_otp_request_timer, is_phone_otp_timer_loading } = usePhoneNumberVerificationSetTimer(true); + const { trackPhoneVerificationEvents } = usePhoneVerificationAnalytics(); + const { localize } = useTranslations(); + + useEffect(() => { + if (show_confirm_phone_number) { + trackPhoneVerificationEvents({ + action: 'open', + subform_name: 'verify_phone_screen', + }); + } + }, [show_confirm_phone_number, trackPhoneVerificationEvents]); + + useEffect(() => { + setPhoneNumber(account_settings?.phone?.replace(/\D/g, '') || ''); + }, [account_settings?.phone]); + + useEffect(() => { + if (email_otp_error) { + invalidate('get_settings').then(() => setIsButtonLoading(false)); + } + if (is_email_verified) { + setIsButtonLoading(false); + setOtpVerification({ show_otp_verification: true, phone_verification_type }); + setShouldShowPhoneNumberOTP(true); + } + }, [is_email_verified, email_otp_error, invalidate]); + + const handleOnChangePhoneNumber = (e: ChangeEvent) => { + setPhoneNumber(e.target.value); + validatePhoneNumber(`+${e.target.value}`, setErrorMessage, setIsDisabledRequestButton); + }; + + const handleSubmit = async (phone_verification_type: string) => { + setIsButtonLoading(true); + setPhoneVerificationType(phone_verification_type); + const { error } = await setUsersPhoneNumber({ phone: `+${phone_number}` }); + + if (!error) { + trackPhoneVerificationEvents({ + action: 'click_cta', + cta_name: + phone_verification_type === VERIFICATION_SERVICES.SMS + ? 'Get code via SMS' + : 'Get code via WhatsApp', + subform_name: 'verify_phone_screen', + }); + phone_verification_type === VERIFICATION_SERVICES.SMS ? requestOnSMS() : requestOnWhatsApp(); + } else { + setIsButtonLoading(false); + } + }; + + const isSingularValue = (time: number) => { + return time === 1; + }; + + const resendPhoneOtpTimer = () => { + let resendPhoneOtpTimer = ''; + if (next_phone_otp_request_timer) { + next_phone_otp_request_timer < 60 + ? (resendPhoneOtpTimer = `${next_phone_otp_request_timer} ${ + isSingularValue(next_phone_otp_request_timer) ? localize('second') : localize('seconds') + }`) + : (resendPhoneOtpTimer = `${Math.round(next_phone_otp_request_timer / 60)} ${ + isSingularValue(Math.round(next_phone_otp_request_timer / 60)) + ? localize('minute') + : localize('minutes') + }`); + } else { + resendPhoneOtpTimer = ''; + } + + return resendPhoneOtpTimer; + }; + + return ( + <> + + + +
+ +
+
+ + +
+ + } + isVisible={!!next_phone_otp_request_timer} + /> + + ); +}); + +export default ConfirmPhoneNumber; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/cool-down-period-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/cool-down-period-modal.tsx new file mode 100644 index 000000000000..a0405afa8a2e --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/cool-down-period-modal.tsx @@ -0,0 +1,48 @@ +import { Modal, Text } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; +import { useDevice } from '@deriv-com/ui'; +import { routes } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { useHistory } from 'react-router'; + +type TCoolDownPeriodModal = { + show_cool_down_period_modal: boolean; + setShowCoolDownPeriodModal: (value: boolean) => void; +}; + +const CoolDownPeriodModal = observer( + ({ show_cool_down_period_modal, setShowCoolDownPeriodModal }: TCoolDownPeriodModal) => { + const { ui } = useStore(); + const history = useHistory(); + const { isMobile } = useDevice(); + const { setIsForcedToExitPnv, setShouldShowPhoneNumberOTP } = ui; + const handleCloseCoolDownPeriodModal = () => { + setShouldShowPhoneNumberOTP(false); + setIsForcedToExitPnv(false); + setShowCoolDownPeriodModal(false); + history.push(routes.personal_details); + }; + return ( + } + primaryButtonCallback={handleCloseCoolDownPeriodModal} + disableCloseOnOverlay + buttonColor='coral' + > + } /> + + + + + + + ); + } +); + +export default CoolDownPeriodModal; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/didnt-get-the-code-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/didnt-get-the-code-modal.tsx new file mode 100644 index 000000000000..518894af1fab --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/didnt-get-the-code-modal.tsx @@ -0,0 +1,106 @@ +import { useEffect } from 'react'; +import { TSocketError } from '@deriv/api/types'; +import { VERIFICATION_SERVICES } from '@deriv/shared'; +import { useTranslations, Localize } from '@deriv-com/translations'; +import { Modal } from '@deriv-com/quill-ui'; +import { useDevice } from '@deriv-com/ui'; +import { convertPhoneTypeDisplay } from '../../../Helpers/utils'; + +type TDidntGetTheCodeModal = { + phone_verification_type: string; + should_show_didnt_get_the_code_modal: boolean; + setIsButtonDisabled: (value: boolean) => void; + setShouldShowDidntGetTheCodeModal: (value: boolean) => void; + requestOnSMS: () => void; + requestOnWhatsApp: () => void; + clearOtpValue: () => void; + is_email_verified: boolean; + email_otp_error: TSocketError<'phone_number_challenge'> | null; + setOtpVerification: (value: { show_otp_verification: boolean; phone_verification_type: string }) => void; + reInitializeGetSettings: () => void; +}; + +const DidntGetTheCodeModal = ({ + should_show_didnt_get_the_code_modal, + setShouldShowDidntGetTheCodeModal, + setIsButtonDisabled, + reInitializeGetSettings, + requestOnSMS, + requestOnWhatsApp, + clearOtpValue, + phone_verification_type, + is_email_verified, + email_otp_error, + setOtpVerification, +}: TDidntGetTheCodeModal) => { + const { isMobile } = useDevice(); + const { localize } = useTranslations(); + + useEffect(() => { + if (is_email_verified || email_otp_error) reInitializeGetSettings(); + }, [is_email_verified, email_otp_error, reInitializeGetSettings]); + + const setDidntGetACodeButtonDisabled = () => { + setIsButtonDisabled(true); + }; + + const handleResendCode = () => { + clearOtpValue(); + setDidntGetACodeButtonDisabled(); + phone_verification_type === VERIFICATION_SERVICES.SMS ? requestOnSMS() : requestOnWhatsApp(); + setOtpVerification({ show_otp_verification: true, phone_verification_type }); + setShouldShowDidntGetTheCodeModal(false); + }; + + const handleChangeOTPVerification = () => { + clearOtpValue(); + setDidntGetACodeButtonDisabled(); + const changed_phone_verification_type = + phone_verification_type === VERIFICATION_SERVICES.SMS + ? VERIFICATION_SERVICES.WHATSAPP + : VERIFICATION_SERVICES.SMS; + + phone_verification_type === VERIFICATION_SERVICES.SMS ? requestOnWhatsApp() : requestOnSMS(); + + setOtpVerification({ + show_otp_verification: true, + phone_verification_type: changed_phone_verification_type, + }); + setShouldShowDidntGetTheCodeModal(false); + }; + + return ( + } + buttonColor='coral' + secondaryButtonLabel={ + + } + showCrossIcon + toggleModal={() => setShouldShowDidntGetTheCodeModal(false)} + > + } /> + + ); +}; + +export default DidntGetTheCodeModal; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/index.ts b/packages/account/src/Sections/Profile/PhoneVerification/index.ts new file mode 100644 index 000000000000..8a1c82b65a3d --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/index.ts @@ -0,0 +1,3 @@ +import PhoneVerificationPage from './phone-verification-page'; + +export default PhoneVerificationPage; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/otp-verification.tsx b/packages/account/src/Sections/Profile/PhoneVerification/otp-verification.tsx new file mode 100644 index 000000000000..78bfd0ee2a6b --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/otp-verification.tsx @@ -0,0 +1,226 @@ +import { useEffect, useState, useCallback, Fragment } from 'react'; +import { + usePhoneNumberVerificationSetTimer, + usePhoneVerificationAnalytics, + useSendOTPVerificationCode, + useSettings, +} from '@deriv/hooks'; +import { Text, InputGroupButton, Button } from '@deriv-com/quill-ui'; +import { observer, useStore } from '@deriv/stores'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { convertPhoneTypeDisplay } from '../../../Helpers/utils'; +import ResendCodeTimer from './resend-code-timer'; +import DidntGetTheCodeModal from './didnt-get-the-code-modal'; +import PhoneNumberVerifiedModal from './phone-number-verified-modal'; +import CoolDownPeriodModal from './cool-down-period-modal'; +import { useIsMounted } from '@deriv/shared'; + +type TOTPVerification = { + phone_verification_type: string; + setOtpVerification: (value: { show_otp_verification: boolean; phone_verification_type: string }) => void; +}; + +const OTPVerification = observer(({ phone_verification_type, setOtpVerification }: TOTPVerification) => { + const { client, ui } = useStore(); + const { setVerificationCode, is_authorize } = client; + const { data: account_settings, invalidate } = useSettings(); + const [should_show_didnt_get_the_code_modal, setShouldShowDidntGetTheCodeModal] = useState(false); + const [otp, setOtp] = useState(''); + const [is_button_disabled, setIsButtonDisabled] = useState(false); + const { trackPhoneVerificationEvents } = usePhoneVerificationAnalytics(); + const { localize } = useTranslations(); + const isMounted = useIsMounted(); + + const { + sendPhoneOTPVerification, + phone_otp_error_message, + setPhoneOtpErrorMessage, + is_phone_number_verified, + is_email_verified, + show_cool_down_period_modal, + setShowCoolDownPeriodModal, + sendEmailOTPVerification, + requestOnSMS, + requestOnWhatsApp, + email_otp_error, + reset, + } = useSendOTPVerificationCode(); + const { + setNextEmailOtpRequestTimer, + is_email_otp_timer_loading, + setNextPhoneOtpRequestTimer, + is_phone_otp_timer_loading, + } = usePhoneNumberVerificationSetTimer(); + const { should_show_phone_number_otp, setIsForcedToExitPnv } = ui; + + const reInitializeGetSettings = useCallback(() => { + invalidate('get_settings').then(() => { + if (isMounted()) { + setIsButtonDisabled(false); + } + }); + }, [invalidate]); + + useEffect(() => { + if (should_show_phone_number_otp) { + trackPhoneVerificationEvents({ + action: 'open', + subform_name: 'verify_phone_otp_screen', + }); + } else { + trackPhoneVerificationEvents({ + action: 'open', + subform_name: 'verify_email_screen', + }); + } + }, [should_show_phone_number_otp, trackPhoneVerificationEvents]); + + useEffect(() => { + if (is_authorize) { + setIsButtonDisabled(true); + reInitializeGetSettings(); + } + }, [reInitializeGetSettings, is_authorize]); + + useEffect(() => { + if (is_phone_number_verified) { + setIsForcedToExitPnv(true); + } else if (is_email_verified && !should_show_phone_number_otp) { + setVerificationCode(otp, 'phone_number_verification'); + setOtpVerification({ show_otp_verification: false, phone_verification_type: '' }); + } + }, [is_phone_number_verified, is_email_verified, setOtpVerification, should_show_phone_number_otp]); + + const clearOtpValue = () => { + setOtp(''); + setPhoneOtpErrorMessage(''); + should_show_phone_number_otp ? setNextPhoneOtpRequestTimer(undefined) : setNextEmailOtpRequestTimer(undefined); + reset(); + }; + + const handleGetOtpValue = (e: React.ChangeEvent) => { + setOtp(e.target.value); + setPhoneOtpErrorMessage(''); + }; + + const handleVerifyOTP = () => { + if (should_show_phone_number_otp) { + trackPhoneVerificationEvents({ + action: 'click_cta', + subform_name: 'verify_phone_otp_screen', + }); + sendPhoneOTPVerification(otp); + } else { + trackPhoneVerificationEvents({ + action: 'click_cta', + subform_name: 'verify_email_screen', + }); + sendEmailOTPVerification(otp); + } + }; + + const isTimerLoading = () => { + return should_show_phone_number_otp ? is_phone_otp_timer_loading : is_email_otp_timer_loading; + }; + + return ( + <> + + + {should_show_phone_number_otp && ( + + )} + + {should_show_phone_number_otp ? ( + + ) : ( + + )} + +
+ {should_show_phone_number_otp ? ( + + + setOtpVerification({ show_otp_verification: false, phone_verification_type }) + } + />, + ]} + /> + + ) : ( + + + ]} + /> + + + + + + )} +
+
+ ) => { + if (e.key === 'Enter') { + handleVerifyOTP(); + } + }} + inputMode='numeric' + buttonColor='coral' + onChange={handleGetOtpValue} + message={phone_otp_error_message} + value={otp} + type='number' + maxLength={6} + buttonDisabled={otp.length < 6} + /> + +
+ + ); +}); + +export default OTPVerification; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/phone-number-verified-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/phone-number-verified-modal.tsx new file mode 100644 index 000000000000..c317fdea0d50 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/phone-number-verified-modal.tsx @@ -0,0 +1,74 @@ +import { useEffect } from 'react'; +import { usePhoneVerificationAnalytics } from '@deriv/hooks'; +import { Modal, Text } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; +import { useDevice } from '@deriv-com/ui'; +import { observer, useStore } from '@deriv/stores'; +import { useHistory } from 'react-router-dom'; +import { routes } from '@deriv/shared'; + +type TPhoneNumberVerifiedModal = { + should_show_phone_number_verified_modal: boolean; +}; + +const PhoneNumberVerifiedModal = observer(({ should_show_phone_number_verified_modal }: TPhoneNumberVerifiedModal) => { + const history = useHistory(); + const previous_route = localStorage.getItem('routes_from_notification_to_pnv'); + const should_route_back_to_previous = + !!previous_route && + (previous_route !== routes.personal_details || previous_route !== routes.phone_verification); + + const handleDoneButton = () => { + localStorage.removeItem('routes_from_notification_to_pnv'); + + should_route_back_to_previous ? history.push(previous_route) : history.push(routes.traders_hub); + }; + + const { isMobile } = useDevice(); + const { + ui, + client: { + account_settings: { phone }, + }, + } = useStore(); + const { setIsPhoneVerificationCompleted } = ui; + const { trackPhoneVerificationEvents } = usePhoneVerificationAnalytics(); + + useEffect(() => { + if (should_show_phone_number_verified_modal) { + trackPhoneVerificationEvents({ + action: 'open', + subform_name: 'verification_successful', + }); + setIsPhoneVerificationCompleted(true); + } + }, [should_show_phone_number_verified_modal, trackPhoneVerificationEvents]); + + return ( + } + disableCloseOnOverlay + > + } /> + +
+ + + +
+
+
+ ); +}); + +export default PhoneNumberVerifiedModal; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-card.tsx b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-card.tsx new file mode 100644 index 000000000000..bf3a515515e0 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-card.tsx @@ -0,0 +1,34 @@ +import { Tag } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; +import { usePhoneNumberVerificationSessionTimer } from '@deriv/hooks'; +import clsx from 'clsx'; +import React from 'react'; + +type TPhoneVerificationCard = { + is_small_card?: boolean; +}; + +const PhoneVerificationCard = ({ children, is_small_card }: React.PropsWithChildren) => { + const { formatted_time } = usePhoneNumberVerificationSessionTimer(); + return ( +
+
+ + } + /> +
+ {children} +
+ ); +}; + +export default PhoneVerificationCard; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-page.tsx b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-page.tsx new file mode 100644 index 000000000000..814b7a7ca403 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification-page.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Loading } from '@deriv/components'; +import { useGrowthbookGetFeatureValue, useIsPhoneNumberVerified, useSendOTPVerificationCode } from '@deriv/hooks'; +import { LabelPairedArrowLeftCaptionFillIcon } from '@deriv/quill-icons'; +import { routes } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Text, IconButton } from '@deriv-com/quill-ui'; +import { useDevice } from '@deriv-com/ui'; +import { Localize } from '@deriv-com/translations'; +import ConfirmPhoneNumber from './confirm-phone-number'; +import CancelPhoneVerificationModal from './cancel-phone-verification-modal'; +import OTPVerification from './otp-verification'; +import SessionTimeoutModal from './session-timeout-modal'; +import VerificationLinkExpiredModal from './verification-link-expired-modal'; +import './phone-verification.scss'; +import PhoneVerificationCard from './phone-verification-card'; + +const PhoneVerificationPage = observer(() => { + const history = useHistory(); + const [otp_verification, setOtpVerification] = useState({ + show_otp_verification: true, + phone_verification_type: '', + }); + const phone_verification_code = sessionStorage.getItem('phone_number_verification_code'); + const [is_loading, setIsLoading] = useState(false); + const [should_show_verification_link_expired_modal, setShouldShowVerificationLinkExpiredModal] = useState(false); + const handleBackButton = () => { + history.push(routes.personal_details); + }; + const { sendEmailOTPVerification, email_otp_error, is_email_verified } = useSendOTPVerificationCode(); + const { is_phone_number_verified } = useIsPhoneNumberVerified(); + const [isPhoneNumberVerificationEnabled, isPhoneNumberVerificationGBLoaded] = useGrowthbookGetFeatureValue({ + featureFlag: 'phone_number_verification', + }); + const { isDesktop } = useDevice(); + const { client, ui } = useStore(); + const { is_redirected_from_email, setRedirectFromEmail, setIsForcedToExitPnv } = ui; + const { + verification_code: { phone_number_verification: phone_number_verification_code }, + is_authorize, + is_virtual, + setVerificationCode, + } = client; + + useEffect(() => { + if ( + (isPhoneNumberVerificationGBLoaded && !isPhoneNumberVerificationEnabled) || + is_virtual || + is_phone_number_verified + ) { + setIsForcedToExitPnv(true); + history.push(routes.personal_details); + } + }, [ + isPhoneNumberVerificationGBLoaded, + isPhoneNumberVerificationEnabled, + is_virtual, + history, + is_phone_number_verified, + ]); + + useEffect(() => { + if (is_redirected_from_email || phone_verification_code) { + setIsLoading(true); + if (email_otp_error) { + setIsLoading(false); + setIsForcedToExitPnv(true); + setShouldShowVerificationLinkExpiredModal(true); + setRedirectFromEmail(false); + sessionStorage.removeItem('phone_number_verification_code'); + } else if (is_email_verified) { + setIsLoading(false); + setOtpVerification({ + show_otp_verification: false, + phone_verification_type: '', + }); + setRedirectFromEmail(false); + sessionStorage.removeItem('phone_number_verification_code'); + } else if ((phone_number_verification_code || phone_verification_code) && is_authorize) { + if (phone_verification_code) setVerificationCode(phone_verification_code, 'phone_number_verification'); + sendEmailOTPVerification(phone_verification_code || phone_number_verification_code); + } + } + }, [ + email_otp_error, + is_email_verified, + phone_number_verification_code, + is_authorize, + is_redirected_from_email, + phone_verification_code, + ]); + + if (is_loading || !isPhoneNumberVerificationGBLoaded) { + return ; + } + + return ( +
+ + {!should_show_verification_link_expired_modal && } + + {isDesktop && ( +
+ + } + /> + + + +
+ )} + + {otp_verification.show_otp_verification ? ( + + ) : ( + + )} + +
+ ); +}); + +export default PhoneVerificationPage; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/phone-verification.scss b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification.scss new file mode 100644 index 000000000000..87bfbe546c18 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/phone-verification.scss @@ -0,0 +1,149 @@ +.phone-verification { + &__get-code-modal, + &__verified-modal, + &__cancel-modal { + &--contents { + display: flex; + flex-direction: column; + gap: 3.2rem; + &__buttons { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + gap: 0.8rem; + margin-top: 2.4rem; + } + } + } + + &__cancel-modal--header { + background-color: var(--core-color-solid-red-100); + } + &__verified-modal--header { + background-color: var(--core-color-solid-green-100); + } + + &__redirect_button { + display: flex; + align-items: center; + + &--text { + padding-inline-start: 1.6rem; + } + + &--icon { + cursor: pointer; + } + } + + &__card { + width: 60rem; + height: 60rem; + border: 1px solid var(--core-color-solid-slate-100, #ebecef); + border-radius: 2 * $BORDER_RADIUS; + display: flex; + flex-direction: column; + align-items: center; + padding: 1.6rem; + margin-top: 2.4rem; + + &--small-card { + height: 40rem; + } + + &--session-timeout-component { + display: flex; + width: 100%; + justify-content: end; + margin-bottom: 2.4rem; + } + + &--inputfield { + width: 60%; + margin-top: 2.4rem; + margin-bottom: 4.4rem; + @include mobile-or-tablet-screen { + margin-top: 3.2rem; + } + @include tablet-screen { + width: 80%; + } + @include mobile-screen { + width: 100%; + } + + &__livechat { + color: var(--core-color-solid-red-900, $color-red-12); + font-weight: bold; + text-decoration: underline; + &:hover { + cursor: pointer; + } + } + } + + &--buttons_container { + width: 100%; + display: flex; + gap: 1.6rem; + margin-top: 0.8rem; + + @include mobile { + flex-direction: column-reverse; + position: fixed; + padding: 0 1.6rem; + bottom: 1.6rem; + } + } + + &--email-verification { + &-content { + width: 100%; + margin-top: 2.4rem; + display: flex; + flex-direction: column; + text-align: center; + } + + &-otp-container { + display: flex; + flex-direction: column; + gap: 1.6rem; + width: 60%; + margin-top: 1.6rem; + align-items: flex-start; + @include mobile-or-tablet-screen { + gap: 3.2rem; + } + @include tablet-screen { + width: 80%; + } + @include mobile-screen { + width: 100%; + } + } + } + + .quill-snackbar { + z-index: 1; + } + + @include mobile { + width: 100vw; + border: none; + margin-top: 0.8rem; + + .dc-input { + width: 100%; + } + } + + @include desktop { + .quill-snackbar { + margin-bottom: 3.2rem; + width: fit-content; + } + } + } +} diff --git a/packages/account/src/Sections/Profile/PhoneVerification/resend-code-timer.tsx b/packages/account/src/Sections/Profile/PhoneVerification/resend-code-timer.tsx new file mode 100644 index 000000000000..0771f78f4ee9 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/resend-code-timer.tsx @@ -0,0 +1,107 @@ +import React, { useCallback } from 'react'; +import { usePhoneNumberVerificationSetTimer, useVerifyEmail } from '@deriv/hooks'; +import { Button, CaptionText } from '@deriv-com/quill-ui'; +import { Localize, useTranslations } from '@deriv-com/translations'; + +type TResendCodeTimer = { + is_button_disabled: boolean; + should_show_resend_code_button: boolean; + setIsButtonDisabled: (value: boolean) => void; + setShouldShowDidntGetTheCodeModal: (value: boolean) => void; + clearOtpValue: () => void; + reInitializeGetSettings: () => void; +}; +const ResendCodeTimer = ({ + is_button_disabled, + should_show_resend_code_button, + clearOtpValue, + setIsButtonDisabled, + setShouldShowDidntGetTheCodeModal, + reInitializeGetSettings, +}: TResendCodeTimer) => { + // @ts-expect-error this for now + const { sendPhoneNumberVerifyEmail, WS, error } = useVerifyEmail('phone_number_verification'); + const { is_request_button_disabled, next_email_otp_request_timer, next_phone_otp_request_timer } = + usePhoneNumberVerificationSetTimer(); + const { localize } = useTranslations(); + + React.useEffect(() => { + if (WS.isSuccess || error) reInitializeGetSettings(); + }, [WS.isSuccess, reInitializeGetSettings, error]); + + const resendCode = () => { + if (should_show_resend_code_button) { + clearOtpValue(); + setIsButtonDisabled(true); + sendPhoneNumberVerifyEmail(); + } else { + setShouldShowDidntGetTheCodeModal(true); + } + }; + + const resendCodeTimer = () => { + let resendCodeTimer = ''; + if (next_email_otp_request_timer) { + next_email_otp_request_timer < 60 + ? (resendCodeTimer = `${localize(' in ')}${next_email_otp_request_timer}s`) + : (resendCodeTimer = `${localize(' in ')}${Math.round(next_email_otp_request_timer / 60)}m`); + } else { + resendCodeTimer = ''; + } + + return resendCodeTimer; + }; + + const didntGetACodeTimer = () => { + let didntGetACodeTimer = ''; + if (next_phone_otp_request_timer) { + next_phone_otp_request_timer < 60 + ? (didntGetACodeTimer = ` (${next_phone_otp_request_timer}s)`) + : (didntGetACodeTimer = ` (${ + next_phone_otp_request_timer && Math.round(next_phone_otp_request_timer / 60) + }m)`); + } else { + didntGetACodeTimer = ''; + } + + return didntGetACodeTimer; + }; + + const isButtonDisabled = useCallback(() => { + const disable_resend_code_button = !!next_email_otp_request_timer || is_button_disabled; + const disable_didnt_get_a_code_button = + !!next_phone_otp_request_timer || is_button_disabled || is_request_button_disabled; + + return should_show_resend_code_button ? disable_resend_code_button : disable_didnt_get_a_code_button; + }, [ + should_show_resend_code_button, + next_email_otp_request_timer, + next_phone_otp_request_timer, + is_button_disabled, + is_request_button_disabled, + ]); + + return ( + + ); +}; + +export default ResendCodeTimer; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/session-timeout-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/session-timeout-modal.tsx new file mode 100644 index 000000000000..37040b4c56dd --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/session-timeout-modal.tsx @@ -0,0 +1,53 @@ +import { useHistory } from 'react-router'; +import { usePhoneNumberVerificationSessionTimer } from '@deriv/hooks'; +import { routes } from '@deriv/shared'; +import { Modal, Text } from '@deriv-com/quill-ui'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { useDevice } from '@deriv-com/ui'; +import { observer, useStore } from '@deriv/stores'; +import { useEffect } from 'react'; + +const SessionTimeoutModal = observer(() => { + const { isMobile } = useDevice(); + const history = useHistory(); + const { localize } = useTranslations(); + const { should_show_session_timeout_modal } = usePhoneNumberVerificationSessionTimer(); + const { ui } = useStore(); + const { is_phone_verification_completed, setShouldShowPhoneNumberOTP, setIsForcedToExitPnv } = ui; + + useEffect(() => { + if (should_show_session_timeout_modal) { + setIsForcedToExitPnv(true); + } + }, [should_show_session_timeout_modal]); + + const redirectBackToPersonalDetails = () => { + setIsForcedToExitPnv(false); + setShouldShowPhoneNumberOTP(false); + history.push(routes.personal_details); + }; + + return ( + } + buttonColor='coral' + title={localize('Session Expired')} + disableCloseOnOverlay + > + } /> + + + + + + + ); +}); + +export default SessionTimeoutModal; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/validation.ts b/packages/account/src/Sections/Profile/PhoneVerification/validation.ts new file mode 100644 index 000000000000..50ceb411fe50 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/validation.ts @@ -0,0 +1,25 @@ +import * as Yup from 'yup'; +import { ValidationConstants } from '@deriv-com/utils'; +import { localize } from '@deriv-com/translations'; + +const phoneNumberSchema = Yup.string().matches( + ValidationConstants.patterns.phoneNumber, + localize('Enter a valid phone number.') +); + +export const validatePhoneNumber = ( + phone_number: string, + setErrorMessage: (value: string) => void, + setIsDisabledRequestButton: (value: boolean) => void +) => { + phoneNumberSchema + .validate(phone_number) + .then(() => { + setErrorMessage(''); + setIsDisabledRequestButton(false); + }) + .catch(({ errors }: any) => { + setErrorMessage(errors); + setIsDisabledRequestButton(true); + }); +}; diff --git a/packages/account/src/Sections/Profile/PhoneVerification/verification-link-expired-modal.tsx b/packages/account/src/Sections/Profile/PhoneVerification/verification-link-expired-modal.tsx new file mode 100644 index 000000000000..1006af277cf8 --- /dev/null +++ b/packages/account/src/Sections/Profile/PhoneVerification/verification-link-expired-modal.tsx @@ -0,0 +1,93 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router'; +import { usePhoneNumberVerificationSetTimer, useSettings, useVerifyEmail } from '@deriv/hooks'; +import { Modal, Text } from '@deriv-com/quill-ui'; +import { routes } from '@deriv/shared'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { useDevice } from '@deriv-com/ui'; +import { observer, useStore } from '@deriv/stores'; + +type TVerificationLinkExpiredModal = { + should_show_verification_link_expired_modal: boolean; + setShouldShowVerificationLinkExpiredModal: (value: boolean) => void; +}; + +const VerificationLinkExpiredModal = observer( + ({ + should_show_verification_link_expired_modal, + setShouldShowVerificationLinkExpiredModal, + }: TVerificationLinkExpiredModal) => { + const history = useHistory(); + const { ui } = useStore(); + const { setIsForcedToExitPnv } = ui; + //@ts-expect-error ignore this until we add it in GetSettings api types + const { sendPhoneNumberVerifyEmail, WS } = useVerifyEmail('phone_number_verification'); + const { next_email_otp_request_timer, is_email_otp_timer_loading, setNextEmailOtpRequestTimer } = + usePhoneNumberVerificationSetTimer(); + const { invalidate } = useSettings(); + const { isMobile } = useDevice(); + const { localize } = useTranslations(); + + const handleCancelButton = () => { + setIsForcedToExitPnv(false); + setShouldShowVerificationLinkExpiredModal(false); + history.push(routes.personal_details); + }; + + const handleSendNewLinkButton = () => { + sendPhoneNumberVerifyEmail(); + setNextEmailOtpRequestTimer(undefined); + setIsForcedToExitPnv(false); + }; + + const sendNewLinkTimer = () => { + let sendNewLinkTimer = ''; + if (next_email_otp_request_timer) { + next_email_otp_request_timer < 60 + ? (sendNewLinkTimer = `${localize(' in ')}${next_email_otp_request_timer}s`) + : (sendNewLinkTimer = `${localize(' in ')}${Math.round(next_email_otp_request_timer / 60)}m`); + } else { + sendNewLinkTimer = ''; + } + + return sendNewLinkTimer; + }; + + useEffect(() => { + if (WS.isSuccess) invalidate('get_settings').then(() => setShouldShowVerificationLinkExpiredModal(false)); + }, [WS.isSuccess, invalidate]); + + return ( + + } + toggleModal={() => setShouldShowVerificationLinkExpiredModal(false)} + showSecondaryButton + secondaryButtonLabel={} + secondaryButtonCallback={handleCancelButton} + > + } /> + +
+ + + +
+
+
+ ); + } +); + +export default VerificationLinkExpiredModal; diff --git a/packages/account/src/Styles/account.scss b/packages/account/src/Styles/account.scss index 3a593971ccef..6eacad76b027 100644 --- a/packages/account/src/Styles/account.scss +++ b/packages/account/src/Styles/account.scss @@ -330,6 +330,10 @@ $MIN_HEIGHT_FLOATING: calc( max-width: unset; } + &--phone-verification-livechat-link { + color: var(--text-general); + } + &--2-cols { display: grid; grid-template-columns: 1fr 1fr; diff --git a/packages/api/types.ts b/packages/api/types.ts index 2f309c23d180..221f5c5af62f 100644 --- a/packages/api/types.ts +++ b/packages/api/types.ts @@ -2300,6 +2300,89 @@ type PasskeysRenameResponse = { [k: string]: unknown; }; +// TODO: remove these mock phone number challenge types after implementing them inside api-types +type PhoneNumberChallengeRequest = { + /** + * Must be `1` + */ + phone_number_challenge: 1; + /** + * The carrier sending the email code. + */ + email_code: string; + /** + * The carrier sending the OTP. + */ + carrier?: 'whatsapp' | 'sms'; + /** + * [Optional] The login id of the user. If left unspecified, it defaults to the initial authorized token's login id. + */ + loginid?: string; + /** + * [Optional] Used to pass data through the websocket, which may be retrieved via the `echo_req` output field. + */ + passthrough?: { + [k: string]: unknown; + }; + /** + * [Optional] Used to map request to response. + */ + req_id?: number; +}; + +type PhoneNumberChallengeResponse = { + phone_number_challenge?: number; + /** + * Echo of the request made. + */ + echo_req: { + [k: string]: unknown; + }; + /** + * Action name of the request made. + */ + msg_type: 'phone_number_challenge'; + /** + * Optional field sent in request to map to response, present only when request contains `req_id`. + */ + req_id?: number; + [k: string]: unknown; +}; + +// TODO: remove these mock phone number challenge types after implementing them inside api-types +type PhoneNumberVerifyRequest = { + /** + * Must be `1` + */ + phone_number_verify: 1; + /** + * The carrier sending the OTP. + */ + otp: string; + /** + * [Optional] Used to map request to response. + */ + req_id?: number; +}; + +type PhoneNumberVerifyResponse = { + /** + * Echo of the request made. + */ + echo_req: { + [k: string]: unknown; + }; + /** + * Action name of the request made. + */ + msg_type: 'phone_number_verify'; + /** + * Optional field sent in request to map to response, present only when request contains `req_id`. + */ + req_id?: number; + [k: string]: unknown; +}; + type ChangeEmailRequest = { change_email: 'verify' | 'update'; new_email: string; @@ -2696,6 +2779,14 @@ type TSocketEndpoints = { request: PayoutCurrenciesRequest; response: PayoutCurrenciesResponse; }; + phone_number_challenge: { + request: PhoneNumberChallengeRequest; + response: PhoneNumberChallengeResponse; + }; + phone_number_verify: { + request: PhoneNumberVerifyRequest; + response: PhoneNumberVerifyResponse; + }; ping: { request: PingRequest; response: PingResponse; diff --git a/packages/components/src/components/form-submit-error-message/form-submit-error-message.tsx b/packages/components/src/components/form-submit-error-message/form-submit-error-message.tsx index 1e787ef729fa..e455f3b8ae25 100644 --- a/packages/components/src/components/form-submit-error-message/form-submit-error-message.tsx +++ b/packages/components/src/components/form-submit-error-message/form-submit-error-message.tsx @@ -5,16 +5,27 @@ import Text from '../text'; type TFormSubmitErrorMessage = { className?: string; - message: string; + message: React.ReactNode; + weight?: string; + text_color?: string; }; -const FormSubmitErrorMessage = ({ className, message }: TFormSubmitErrorMessage) => ( -
- - - {message} - -
-); +const FormSubmitErrorMessage = ({ + className, + message, + text_color = 'prominent', + weight = 'bold', +}: TFormSubmitErrorMessage) => { + return ( +
+ + { + + {message} + + } +
+ ); +}; export default FormSubmitErrorMessage; diff --git a/packages/components/src/components/open-livechat-link/open-livechat-link.tsx b/packages/components/src/components/open-livechat-link/open-livechat-link.tsx index e845b32247c7..22fa92aca117 100644 --- a/packages/components/src/components/open-livechat-link/open-livechat-link.tsx +++ b/packages/components/src/components/open-livechat-link/open-livechat-link.tsx @@ -2,13 +2,19 @@ import React from 'react'; import { Localize } from '@deriv/translations'; import Text from '../text'; import './open-livechat-link.scss'; +import clsx from 'clsx'; type TOpenLiveChatLink = { text_size?: React.ComponentProps['size']; + className?: string; }; -const OpenLiveChatLink = ({ children, text_size }: React.PropsWithChildren) => ( -