diff --git a/packages/account/src/Components/file-uploader-container/file-uploader-container.jsx b/packages/account/src/Components/file-uploader-container/file-uploader-container.jsx index 24924c1245dd..c097ea9ce509 100644 --- a/packages/account/src/Components/file-uploader-container/file-uploader-container.jsx +++ b/packages/account/src/Components/file-uploader-container/file-uploader-container.jsx @@ -37,7 +37,7 @@ const FileProperties = () => { ); }; -const FileUploaderContainer = ({ is_description_enabled = true, getSocket, onFileDrop, onRef }) => { +const FileUploaderContainer = ({ is_description_enabled = true, getSocket, onFileDrop, onRef, values }) => { const { is_appstore } = React.useContext(PlatformContext); const ref = React.useRef(); @@ -118,7 +118,7 @@ const FileUploaderContainer = ({ is_description_enabled = true, getSocket, onFil 'account-poa__upload-file--dashboard': isDesktop() && is_appstore, })} > - + ); @@ -129,6 +129,7 @@ FileUploaderContainer.propTypes = { getSocket: PropTypes.func, onFileDrop: PropTypes.func, onRef: PropTypes.func, + values: PropTypes.array, }; export default FileUploaderContainer; diff --git a/packages/account/src/Components/file-uploader-container/file-uploader.jsx b/packages/account/src/Components/file-uploader-container/file-uploader.jsx index 24ac7125ee86..a9c5fb265851 100644 --- a/packages/account/src/Components/file-uploader-container/file-uploader.jsx +++ b/packages/account/src/Components/file-uploader-container/file-uploader.jsx @@ -27,8 +27,8 @@ const fileReadErrorMessage = filename => { return localize('Unable to read file {{name}}', { name: filename }); }; -const FileUploader = React.forwardRef(({ onFileDrop, getSocket }, ref) => { - const [document_file, setDocumentFile] = useStateCallback({ files: [], error_message: null }); +const FileUploader = React.forwardRef(({ onFileDrop, getSocket, values }, ref) => { + const [document_file, setDocumentFile] = useStateCallback({ files: values || [], error_message: null }); const handleAcceptedFiles = files => { if (files.length > 0) { @@ -125,6 +125,7 @@ FileUploader.displayName = 'FileUploader'; FileUploader.propTypes = { onFileDrop: PropTypes.func, getSocket: PropTypes.func, + values: PropTypes.array, }; export default FileUploader; diff --git a/packages/account/src/Styles/account.scss b/packages/account/src/Styles/account.scss index b59c82bb2051..0ac95ca97a52 100644 --- a/packages/account/src/Styles/account.scss +++ b/packages/account/src/Styles/account.scss @@ -2749,7 +2749,7 @@ $MIN_HEIGHT_FLOATING: calc( .financial-banner { width: 49.5rem; border-radius: 4px; - position: relaitve; + position: relative; background-color: var(--status-warning-transparent); @include mobile { diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.scss b/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.scss index 45566809332d..29abb89f3e84 100644 --- a/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.scss +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.scss @@ -10,6 +10,15 @@ &-hint { margin-top: 0.8rem; } + &-form-container { + margin: 3.2rem auto 0; + width: 42.2rem; + + @include mobile { + margin-top: 2.4rem; + width: 100%; + } + } &-country-dropdown { margin-top: 3.2rem; @@ -19,7 +28,33 @@ .is-desktop { margin: 0 auto; - width: 422px; + width: 42.2rem; + } + } + &-address { + .dc-autocomplete { + margin-bottom: 3.2em; + } + + &-upload-section { + margin-top: 1.2em; + + &-list { + margin-top: 1.4em; + + li { + display: flex; + align-items: center; + justify-content: flex-start; + + &:before { + content: '\2022'; + color: $color-grey-1; + font-weight: bold; + margin-right: 0.8em; + } + } + } } } &-selector-hint { diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.tsx b/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.tsx index fc3c8d9df1d2..b190521462df 100644 --- a/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.tsx +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/signup-wizard.tsx @@ -9,6 +9,7 @@ import CancelWizardDialog from './components/cancel-wizard-dialog'; import CountryOfIssue from './steps/country-of-issue'; import IdentityVerification from './steps/identity-verification'; import Selfie from './steps/selfie'; +import AddressVerification from './steps/address-verification'; import { populateVerificationStatus } from './helpers/verification'; import { usePaymentAgentSignupReducer } from './steps/steps-reducer'; import './signup-wizard.scss'; @@ -30,6 +31,8 @@ const SignupWizard = ({ account_status, closeWizard }: TSignupWizardProps) => { setIDVData, setManualData, setIsIdentitySubmissionDisabled, + setAddress, + setIsAddressVerificationDisabled, } = usePaymentAgentSignupReducer(); const is_final_step = current_step_key === 'complete_step'; @@ -113,6 +116,18 @@ const SignupWizard = ({ account_status, closeWizard }: TSignupWizardProps) => { > + + + <> diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/address-verification.tsx b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/address-verification.tsx new file mode 100644 index 000000000000..9853ac5de785 --- /dev/null +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/address-verification.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Text, DesktopWrapper } from '@deriv/components'; +import { Localize } from '@deriv/translations'; +import ProofOfAddressForm from './proof-of-address-form'; +import type { TAddress } from './proof-of-address-form/proof-of-address-form'; + +type TAddressVerificationProps = { + address?: TAddress; + onSelect: React.ComponentProps['onSelect']; + setIsAddressVerificationDisabled: React.ComponentProps< + typeof ProofOfAddressForm + >['setIsAddressVerificationDisabled']; + selected_country_id: string; +}; + +const AddressVerification = ({ + address, + onSelect, + setIsAddressVerificationDisabled, + selected_country_id, +}: TAddressVerificationProps) => { + return ( + + + + + + + + + + + + + + + ); +}; + +export default AddressVerification; diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/index.ts b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/index.ts new file mode 100644 index 000000000000..ab226ab71628 --- /dev/null +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/index.ts @@ -0,0 +1,3 @@ +import AddressVerification from './address-verification'; + +export default AddressVerification; diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/index.ts b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/index.ts new file mode 100644 index 000000000000..e1c0e43ff965 --- /dev/null +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/index.ts @@ -0,0 +1,3 @@ +import ProofOfAddressForm from './proof-of-address-form'; + +export default ProofOfAddressForm; diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.scss b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.scss new file mode 100644 index 000000000000..143b34852812 --- /dev/null +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.scss @@ -0,0 +1,226 @@ +.account-poa { + &__upload { + &-icon { + margin-bottom: 1rem; + @include mobile { + margin-bottom: 0; + } + } + + &-dashboard { + max-width: calc(100% - 42rem); + } + + &-list { + width: 100%; + flex: 1; + margin-right: 1.6rem; + display: flex; + flex-wrap: wrap; + + .account-poa__upload-box { + border: 1px solid var(--icon-grey-background); + border-radius: $BORDER_RADIUS * 2; + margin: 0 0.8rem 1.6rem; + padding: 0.8rem; + @include mobile { + border: none; + margin: 0; + } + } + + @include mobile { + flex-direction: column; + margin-right: 0; + } + } + + &-file { + flex: 1; + height: 24rem; + position: relative; + margin: 0 0.8rem; + + &-dashboard { + height: 10.5rem; + + & .account-poa__upload-remove-btn { + top: 25rem; + right: 1rem; + } + } + + &-zone { + max-width: 40rem; + height: 24rem; + padding: 0.8rem; + } + + .dc-file-dropzone { + border: 1px solid var(--general-active); + + &__message { + max-width: unset; + + &-subtitle { + font-size: 1.4rem; + font-weight: bold; + } + } + + @include mobile { + border: 1px dashed var(--icon-grey-background); + } + } + + @include mobile { + flex: unset; + margin-bottom: 2.4rem; + } + } + + &-property { + display: grid; + grid-template-areas: + 'file-size file-size file-format file-format file-time file-time' + 'file-clear file-clear file-clear file-address file-address file-address'; + + &-wrapper { + align-self: center; + align-items: center; + display: flex; + flex-direction: column; + padding: 2.4rem; + } + + &-item { + border-radius: 0.4rem; + height: 10.3rem; + border: 1px solid var(--border-normal); + margin: 0.8rem; + display: flex; + flex-direction: column; + justify-content: center; + } + + &-size { + grid-area: file-size; + } + + &-format { + grid-area: file-format; + } + + &-time { + grid-area: file-time; + } + + &-clear { + grid-area: file-clear; + } + + &-with-address { + grid-area: file-address; + } + } + + &-remove-btn { + position: absolute; + width: 1.6rem; + height: 1.6rem; + top: 0.8rem; + right: 0.8rem; + cursor: pointer; + transition: transform 0.25s linear; + + &:hover { + transform: scale(1.25, 1.25); + } + + &--error { + circle { + fill: var(--status-danger); + } + } + + &-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + + &-box { + display: flex; + flex-direction: column; + margin-bottom: 2em; + text-align: center; + align-items: center; + justify-content: flex-start; + flex: 1 0 calc((100% / 3) - 3.2rem); + + &-dashboard { + flex-direction: column !important; + margin-left: 1.6rem; + + & span { + display: list-item; + list-style-type: disc; + margin-bottom: 0.8rem; + } + + @include mobile { + margin-top: 1.6rem; + margin-bottom: 0; + } + } + + @include mobile { + text-align: left; + flex-direction: row; + } + } + + &-item { + width: 100%; + padding: 0 0.5em; + font-size: var(--text-size-xxs); + line-height: 1.5; + color: var(--text-prominent); + } + } + + &__details { + &-section { + margin-top: 1em; + display: flex; + flex-wrap: wrap; + } + + &-description { + width: 100%; + margin-right: 0.5em; + flex: 1; + + @include mobile { + margin-bottom: 2.4rem; + } + } + + &-fields { + width: 100%; + min-width: 400px; + flex: 1; + + .account-form__fieldset { + margin-bottom: 4em; // The gap for the error message is dependent on the font-size, better to be `em` instead of `rem` + } + + @include mobile { + min-width: 100%; + } + } + } +} diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.tsx b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.tsx new file mode 100644 index 000000000000..6647f6794ca8 --- /dev/null +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/address-verification/proof-of-address-form/proof-of-address-form.tsx @@ -0,0 +1,315 @@ +import React from 'react'; +import { StatesList, StatesListResponse } from '@deriv/api-types'; +import { isEmptyObject, WS, validAddress, validPostCode, validLetterSymbol, validLength } from '@deriv/shared'; +import { Autocomplete, Input, DesktopWrapper, MobileWrapper, SelectNative, Text, Loading } from '@deriv/components'; +import { Formik, Field, FieldProps } from 'formik'; +import { localize, Localize } from '@deriv/translations'; +import { useStore } from '@deriv/stores'; +import { FileUploaderContainer } from '@deriv/account'; +import { TReactChangeEvent } from 'Types'; +import './proof-of-address-form.scss'; + +export type TPOAFormValues = { + address_line_1: string; + address_line_2: string; + address_city: string; + address_state: string; + address_postcode: string | number; +}; + +export type TAddress = TPOAFormValues & { + proof_of_address: TProofFiles | null; +}; + +type TProofFiles = { + files: File[]; + error_message: string | null; +}; + +type TProofOfAddressFormProps = { + address?: TAddress; + onSelect: (value: TAddress) => void; + setIsAddressVerificationDisabled: (value: boolean) => void; + selected_country_id?: string; +}; + +const validate = + (errors: Partial, values: TPOAFormValues) => + (fn: (value: string) => string, arr: string[], err_msg: string) => { + arr.forEach(field => { + const value = values[field as keyof TPOAFormValues]; + if (!fn(value as string) && !errors[field as keyof TPOAFormValues] && !!err_msg !== true) + errors[field as keyof TPOAFormValues] = err_msg; + }); + }; + +let file_uploader_ref = null; + +const ProofOfAddressForm = ({ + address, + onSelect, + setIsAddressVerificationDisabled, + selected_country_id, +}: TProofOfAddressFormProps) => { + const [document_file, setDocumentFile] = React.useState( + address?.proof_of_address || { files: [], error_message: null } + ); + const [states_list, setStatesList] = React.useState(); + const [is_loading, setIsLoading] = React.useState(true); + const [is_form_filled, setIsFormFilled] = React.useState(false); + const { + client: { fetchStatesList }, + } = useStore(); + + React.useEffect(() => { + fetchStatesList(selected_country_id).then((response: StatesListResponse) => { + setStatesList(response.states_list); + setIsLoading(false); + }); + }, [selected_country_id, fetchStatesList]); + + React.useEffect(() => { + setIsAddressVerificationDisabled(!document_file.files.length || !is_form_filled); + }, [is_form_filled, document_file, setIsAddressVerificationDisabled]); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { address_line_1, address_line_2, address_city, address_state, address_postcode } = address!; + + const form_initial_values = { + address_line_1, + address_line_2, + address_city, + address_state, + address_postcode, + }; + + const validateFields = (values: TPOAFormValues) => { + const errors: Partial = {}; + const validateValues = validate(errors, values); + const permitted_characters = ". , ' : ; ( ) @ # / -"; + const required_fields = ['address_line_1', 'address_city', 'address_state', 'address_postcode']; + const error_msg = { + required: localize('This field is required'), + validation_letter_symbol_message: localize( + 'Only letters, space, hyphen, period, and apostrophe are allowed.' + ), + address_validation_message: localize( + 'Use only the following special characters: {{ permitted_characters }}', + { + permitted_characters, + interpolation: { escapeValue: false }, + } + ), + address_postcode: localize('Only letters, numbers, space, and hyphen are allowed.'), + address_postcode_length: localize('Please enter a {{field_name}} under {{max_number}} characters.', { + field_name: localize('Postal/ZIP code'), + max_number: 20, + interpolation: { escapeValue: false }, + }), + }; + + validateValues(val => val, required_fields, error_msg.required); + + if (values.address_line_1 && !validAddress(values.address_line_1)) { + errors.address_line_1 = error_msg.address_validation_message; + } + if (values.address_line_2 && !validAddress(values.address_line_2)) { + errors.address_line_2 = error_msg.address_validation_message; + } + + if (values.address_city && !validLetterSymbol(values.address_city)) { + errors.address_city = error_msg.validation_letter_symbol_message; + } + + // only add state/province validation for countries that don't have states list fetched from API + if (values.address_state && !validLetterSymbol(values.address_state) && states_list && states_list.length < 1) { + errors.address_state = error_msg.validation_letter_symbol_message; + } + + if (values.address_postcode) { + if (!validLength(values.address_postcode, { min: 0, max: 20 })) { + errors.address_postcode = error_msg.address_postcode_length; + } else if (!validPostCode(values.address_postcode)) { + errors.address_postcode = error_msg.address_postcode; + } + } + + onSelect({ + ...values, + proof_of_address: document_file, + }); + setIsFormFilled( + isEmptyObject(errors) && + !Object.keys(values).some( + key => required_fields.includes(key) && values[key as keyof TPOAFormValues] === '' + ) + ); + + return errors; + }; + + const onSubmitValues = (values: TPOAFormValues) => { + onSelect({ + ...values, + proof_of_address: document_file, + }); + }; + + if (is_loading) return ; + + return ( + + {({ values, errors, touched, handleChange, handleBlur, handleSubmit, setFieldValue }) => ( +
+
+
+ +
+
+ +
+
+ +
+
+ {states_list && states_list.length ? ( + + + + {({ field }: FieldProps) => ( + setFieldValue('address_state', value ? text : '', true)} + /> + )} + + + + + setFieldValue('address_state', e.target.value, true) + } + /> + + + ) : ( + + )} +
+
+ +
+
+ +
+ + + +
    +
  • + + + +
  • +
  • + + + +
  • +
+
+ (file_uploader_ref = ref)} + onFileDrop={(df: TProofFiles) => { + onSelect({ + ...address!, + proof_of_address: { files: df.files, error_message: df.error_message }, + }); + setDocumentFile({ files: df.files, error_message: df.error_message }); + }} + getSocket={WS.getSocket} + values={address?.proof_of_address?.files} + /> +
+
+
+ )} +
+ ); +}; + +export default ProofOfAddressForm; diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/steps-reducer.ts b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/steps-reducer.ts index 0c1754e49112..2806c45f7e7e 100644 --- a/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/steps-reducer.ts +++ b/packages/cashier/src/pages/payment-agent/payment-agent-signup/steps/steps-reducer.ts @@ -2,6 +2,7 @@ import { useCallback, useReducer } from 'react'; import { isEmptyObject } from '@deriv/shared'; import { ResidenceList } from '@deriv/api-types'; import { TSelfie } from './selfie/selfie'; +import type { TAddress } from './address-verification/proof-of-address-form/proof-of-address-form'; import { Moment } from 'moment'; //TODO: refactor TSelfie type into TUploadedDocumentType @@ -61,6 +62,8 @@ export type TStepsState = { selfie: { selfie_with_id: TSelfie; } | null; + address?: TAddress; + is_address_verification_disabled?: boolean; }; // Action creators @@ -106,8 +109,30 @@ const setSelectedManualDocumentIndexAC = (value: string) => { } as const; }; +const setAddressAC = (value?: TAddress) => { + return { + type: 'SET_ADDRESS', + value, + } as const; +}; + +const setIsAddressVerificationDisabledAC = (value?: boolean) => { + return { + type: 'SET_IS_ADDRESS_VERIFICATION_DISABLED', + value, + } as const; +}; + // Initial state const initial_state = { + address: { + address_line_1: '', + address_line_2: '', + address_city: '', + address_state: '', + address_postcode: '', + proof_of_address: null, + }, idv_data: { values: { document_number: '', @@ -116,6 +141,7 @@ const initial_state = { errors: { document_number: '', document_type: '' }, country_code: '', }, + is_address_verification_disabled: true, is_identity_submission_disabled: true, manual_data: { values: {}, errors: {} }, selected_country: {}, @@ -130,6 +156,10 @@ const stepsReducer = (state: TStepsState, action: TActionsTypes): TStepsState => return { ...state, selfie: { selfie_with_id: action.value } }; case 'SET_SELECTED_COUNTRY': return { ...state, selected_country: action.value }; + case 'SET_ADDRESS': + return { ...state, address: action.value }; + case 'SET_IS_ADDRESS_VERIFICATION_DISABLED': + return { ...state, is_address_verification_disabled: action.value }; case 'SET_IS_IDENTITY_SUBMISSION_DISABLED': return { ...state, is_identity_submission_disabled: action.value }; case 'SET_IDV_DATA': { @@ -177,23 +207,32 @@ export const usePaymentAgentSignupReducer = () => { (value: boolean) => dispatch(setIsIdentitySubmissionDisabledAC(value)), [] ); + const setAddress = useCallback((value: TAddress) => dispatch(setAddressAC(value)), []); + const setIsAddressVerificationDisabled = useCallback( + (value: boolean) => dispatch(setIsAddressVerificationDisabledAC(value)), + [] + ); return { steps_state, + setAddress, setIDVData, + setIsAddressVerificationDisabled, + setIsIdentitySubmissionDisabled, setManualData, setSelectedCountry, setSelectedManualDocumentIndex, setSelfie, - setIsIdentitySubmissionDisabled, }; }; type TActionsTypes = ReturnType< - | typeof setSelfieAC - | typeof setSelectedCountryAC - | typeof setSelectedManualDocumentIndexAC + | typeof setAddressAC | typeof setIDVDataAC - | typeof setManualDataAC + | typeof setIsAddressVerificationDisabledAC | typeof setIsIdentitySubmissionDisabledAC + | typeof setManualDataAC + | typeof setSelectedCountryAC + | typeof setSelectedManualDocumentIndexAC + | typeof setSelfieAC >; diff --git a/packages/cashier/src/types/stores/client-store.types.ts b/packages/cashier/src/types/stores/client-store.types.ts index ea890b4ae287..faa1be65d362 100644 --- a/packages/cashier/src/types/stores/client-store.types.ts +++ b/packages/cashier/src/types/stores/client-store.types.ts @@ -3,6 +3,8 @@ import { Authorize, ResidenceList, CountriesListResponse, + StatesList, + StatesListResponse, DetailsOfEachMT5Loginid, } from '@deriv/api-types'; @@ -28,6 +30,7 @@ export type TClientStore = { current_currency_type?: string; current_fiat_currency?: string; fetchResidenceList: () => Promise; + fetchStatesList: (residence_id?: string) => Promise; getLimits: () => void; is_account_setting_loaded: boolean; is_eu: boolean; @@ -55,6 +58,7 @@ export type TClientStore = { standpoint: { iom: string; }; + states_list: StatesList; switchAccount: (value?: string) => void; verification_code: { payment_agent_withdraw: string; diff --git a/packages/components/stories/icon/icons.js b/packages/components/stories/icon/icons.js index 8a6ce7efa518..78d460ba1866 100644 --- a/packages/components/stories/icon/icons.js +++ b/packages/components/stories/icon/icons.js @@ -581,9 +581,6 @@ export const icons = 'IcCurrencyUst', 'IcCurrencyVirtual', ], - 'derivez': [ - 'IcDerivez', - ], 'dxtrade': [ 'IcDxtradeDerivX', 'IcDxtradeDerived', diff --git a/packages/core/src/Stores/client-store.js b/packages/core/src/Stores/client-store.js index 1b8cb1a4fff9..20bcfe0492da 100644 --- a/packages/core/src/Stores/client-store.js +++ b/packages/core/src/Stores/client-store.js @@ -2226,11 +2226,11 @@ export default class ClientStore extends BaseStore { this.residence_list = residence_list_response.residence_list || []; } - fetchStatesList() { + fetchStatesList(resident_id) { return new Promise((resolve, reject) => { WS.authorized.storage .statesList({ - states_list: this.accounts[this.loginid].residence, + states_list: resident_id || this.accounts[this.loginid].residence, }) .then(response => { if (response.error) {