diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/components/access-method-picker.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/components/access-method-picker.tsx
index f109d6e88c0f85..235f95fa58f865 100644
--- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/components/access-method-picker.tsx
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/components/access-method-picker.tsx
@@ -14,7 +14,7 @@ export const AccessMethodPicker: FC< CredentialsFormFieldProps > = ( { control }
(
= ( { control }
(
= ( {
return (
-
+
{ isEnglishLocale ? translate( 'Current site address' ) : translate( 'Site address' ) }
(
= ( {
/>
) }
/>
-
+
);
};
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/hooks/use-form-error-mapping.ts b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/hooks/use-form-error-mapping.ts
new file mode 100644
index 00000000000000..e47e66d98edec8
--- /dev/null
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/hooks/use-form-error-mapping.ts
@@ -0,0 +1,81 @@
+import { useTranslate } from 'i18n-calypso';
+import { useCallback, useMemo } from 'react';
+import { FieldErrors } from 'react-hook-form';
+import { ApiError, CredentialsFormData } from '../types';
+
+// This function is used to map the error message to the correct field in the form.
+// Backend is returning the errors related to backup files using 'from_url' key
+// but we need to use 'backupFileLocation' to identify the field in the form.
+const getFieldName = ( key: string, migrationType: string ) => {
+ return 'backup' === migrationType && key === 'from_url' ? 'backupFileLocation' : key;
+};
+
+// ** This hook is used to map the error messages to the form fields errors.
+export const useFormErrorMapping = (
+ error?: ApiError | null,
+ variables?: CredentialsFormData | null
+): FieldErrors< CredentialsFormData > | undefined => {
+ const translate = useTranslate();
+
+ const fieldMapping: Record< string, { type: string; message: string } | null > = useMemo(
+ () => ( {
+ from_url: { type: 'manual', message: translate( 'Enter a valid URL.' ) },
+ username: { type: 'manual', message: translate( 'Enter a valid username.' ) },
+ password: { type: 'manual', message: translate( 'Enter a valid password.' ) },
+ backupFileLocation: { type: 'manual', message: translate( 'Enter a valid URL.' ) },
+ } ),
+ [ translate ]
+ );
+
+ const getTranslatedMessage = useCallback(
+ ( key: string ) => {
+ return (
+ fieldMapping[ key ] ?? {
+ type: 'manual',
+ message: translate( 'Invalid input, please check again' ),
+ }
+ );
+ },
+ [ fieldMapping, translate ]
+ );
+
+ const handleServerError = useCallback(
+ ( error: ApiError, { migrationType }: CredentialsFormData ) => {
+ const { code, message, data } = error;
+
+ if ( code === 'rest_missing_callback_param' || ! code ) {
+ return {
+ root: {
+ type: 'manual',
+ message: translate( 'An error occurred while saving credentials.' ),
+ },
+ };
+ }
+
+ if ( code !== 'rest_invalid_param' || ! data?.params ) {
+ return { root: { type: 'manual', message } };
+ }
+
+ const invalidFields = Object.keys( data.params );
+
+ return invalidFields.reduce(
+ ( errors, key ) => {
+ const fieldName = getFieldName( key, migrationType );
+ const message = getTranslatedMessage( key );
+
+ errors[ fieldName ] = message;
+ return errors;
+ },
+ {} as Record< string, { type: string; message: string } >
+ );
+ },
+ [ getTranslatedMessage, translate ]
+ );
+
+ return useMemo( () => {
+ if ( error && variables ) {
+ return handleServerError( error, variables ) as FieldErrors< CredentialsFormData >;
+ }
+ return undefined;
+ }, [ error, handleServerError, variables ] );
+};
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx
index b171f607586d31..af24545c401048 100644
--- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx
@@ -128,13 +128,20 @@ describe( 'SiteMigrationCredentials', () => {
expect( wpcomRequest ).not.toHaveBeenCalled();
} );
- it( 'shows errors on the required fields when the user does not fill the fields', async () => {
+ it( 'shows errors on the required fields when the user does not fill the fields when user select credentials option', async () => {
render();
await userEvent.click( continueButton() );
+ await userEvent.click( credentialsOption() );
+ expect( getByText( messages.urlError ) ).toBeVisible();
+ expect( getByText( messages.usernameError ) ).toBeVisible();
+ expect( getByText( messages.passwordError ) ).toBeVisible();
+ } );
- expect( getByText( messages.urlError ) ).toBeInTheDocument();
- expect( getByText( messages.usernameError ) ).toBeInTheDocument();
- expect( getByText( messages.passwordError ) ).toBeInTheDocument();
+ it( 'shows errors on the required fields when the user does not fill the fields when user select backup option', async () => {
+ render();
+ await userEvent.click( backupOption() );
+ await userEvent.click( continueButton() );
+ expect( getByText( /Please enter a valid URL/ ) ).toBeVisible();
} );
it( 'shows error when user set invalid site address', async () => {
@@ -142,7 +149,7 @@ describe( 'SiteMigrationCredentials', () => {
await userEvent.type( siteAddressInput(), 'invalid-site-address' );
await userEvent.click( continueButton() );
- expect( getByText( messages.noTLDError ) ).toBeInTheDocument();
+ expect( getByText( messages.noTLDError ) ).toBeVisible();
} );
it( 'fills the site address and disable it when the user already informed the site address on previous step', async () => {
@@ -214,10 +221,26 @@ describe( 'SiteMigrationCredentials', () => {
await fillAllFields();
await userEvent.click( continueButton() );
- expect( getByText( /Error message from backend/ ) ).toBeVisible();
+ await waitFor( () => {
+ expect( getByText( /Error message from backend/ ) ).toBeVisible();
+ } );
expect( submit ).not.toHaveBeenCalled();
} );
+ it( 'shows an generic error when server doesn`t return error', async () => {
+ const submit = jest.fn();
+ render( { navigation: { submit } } );
+
+ ( wpcomRequest as jest.Mock ).mockRejectedValue( {} );
+
+ await fillAllFields();
+ await userEvent.click( continueButton() );
+
+ await waitFor( () => {
+ expect( getByText( /An error occurred while saving credentials./ ) ).toBeVisible();
+ } );
+ } );
+
it( 'shows a notice when URL contains error=ticket-creation', async () => {
const submit = jest.fn();
const initialEntry = '/site-migration-credentials?error=ticket-creation';
@@ -227,6 +250,9 @@ describe( 'SiteMigrationCredentials', () => {
const errorMessage = await findByText(
/We ran into a problem submitting your details. Please try again shortly./
);
- expect( errorMessage ).toBeVisible();
+
+ await waitFor( () => {
+ expect( errorMessage ).toBeVisible();
+ } );
} );
} );
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/types.ts b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/types.ts
index e6a61abb160054..a3f32c8ec54e50 100644
--- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/types.ts
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/types.ts
@@ -1,6 +1,15 @@
import { Control, FieldErrors } from 'react-hook-form';
export interface CredentialsFormData {
+ from_url: string;
+ username: string;
+ password: string;
+ backupFileLocation: string;
+ migrationType: 'credentials' | 'backup';
+ notes: string;
+}
+
+export interface ApiFormData {
siteAddress: string;
username: string;
password: string;
@@ -9,6 +18,14 @@ export interface CredentialsFormData {
howToAccessSite: 'credentials' | 'backup';
}
+export interface ApiError {
+ code: string;
+ message: string;
+ data: {
+ params?: Record< string, string >;
+ };
+}
+
export interface CredentialsFormFieldProps {
control: Control< CredentialsFormData >;
errors?: FieldErrors< CredentialsFormData >;
@@ -23,5 +40,4 @@ export interface MigrationError {
params?: Record< string, string >;
};
};
- status: number;
}
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-credentials-form.ts b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-credentials-form.ts
index a4f08b4281c6ff..c1eb87c47a0db9 100644
--- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-credentials-form.ts
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-credentials-form.ts
@@ -1,115 +1,59 @@
-import { useTranslate } from 'i18n-calypso';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
-import { MigrationError, CredentialsFormData } from './types';
+import { useFormErrorMapping } from './hooks/use-form-error-mapping';
+import { CredentialsFormData } from './types';
import { useSiteMigrationCredentialsMutation } from './use-site-migration-credentials-mutation';
-const mapApiError = ( error: any ) => {
- return {
- body: {
- code: error.code,
- message: error.message,
- data: error.data,
- },
- status: error.status,
- };
-};
-
export const useCredentialsForm = ( onSubmit: () => void ) => {
- const translate = useTranslate();
const importSiteQueryParam = useQuery().get( 'from' ) || '';
+ const {
+ isPending,
+ mutate: requestAutomatedMigration,
+ error,
+ isSuccess,
+ variables,
+ } = useSiteMigrationCredentialsMutation();
- const fieldMapping = {
- from_url: {
- fieldName: 'siteAddress',
- errorMessage: translate( 'Enter a valid URL.' ),
- },
- username: {
- fieldName: 'username',
- errorMessage: translate( 'Enter a valid username.' ),
- },
- password: {
- fieldName: 'password',
- errorMessage: translate( 'Enter a valid password.' ),
- },
- migration_type: {
- fieldName: 'howToAccessSite',
- errorMessage: null,
- },
- notes: {
- fieldName: 'notes',
- errorMessage: null,
- },
- };
-
- const setGlobalError = ( message?: string | null | undefined ) => {
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- setError( 'root', {
- type: 'manual',
- message: message ?? translate( 'An error occurred while saving credentials.' ),
- } );
- };
-
- const handleMigrationError = ( err: MigrationError ) => {
- let hasUnmappedFieldError = false;
-
- if ( err.body?.code === 'rest_invalid_param' && err.body?.data?.params ) {
- Object.entries( err.body.data.params ).forEach( ( [ key ] ) => {
- const field = fieldMapping[ key as keyof typeof fieldMapping ];
- const keyName =
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- 'backup' === accessMethod && field?.fieldName === 'siteAddress'
- ? 'backupFileLocation'
- : field?.fieldName;
-
- if ( keyName ) {
- const message = field?.errorMessage ?? translate( 'Invalid input, please check again' );
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- setError( keyName as keyof CredentialsFormData, { type: 'manual', message } );
- } else if ( ! hasUnmappedFieldError ) {
- hasUnmappedFieldError = true;
- setGlobalError();
- }
- } );
- } else {
- setGlobalError( err.body?.message );
- }
- };
-
- const { isPending, requestAutomatedMigration } = useSiteMigrationCredentialsMutation( {
- onSuccess: () => {
- recordTracksEvent( 'calypso_site_migration_automated_request_success' );
- onSubmit();
- },
- onError: ( error: any ) => {
- handleMigrationError( mapApiError( error ) );
- recordTracksEvent( 'calypso_site_migration_automated_request_error' );
- },
- } );
+ const serverSideError = useFormErrorMapping( error, variables );
const {
formState: { errors },
control,
handleSubmit,
watch,
- setError,
clearErrors,
} = useForm< CredentialsFormData >( {
mode: 'onSubmit',
reValidateMode: 'onSubmit',
disabled: isPending,
defaultValues: {
- siteAddress: importSiteQueryParam,
+ from_url: importSiteQueryParam,
username: '',
password: '',
backupFileLocation: '',
notes: '',
- howToAccessSite: 'credentials',
+ migrationType: 'credentials',
},
+ errors: serverSideError,
} );
+ const accessMethod = watch( 'migrationType' );
+
+ useEffect( () => {
+ if ( isSuccess ) {
+ recordTracksEvent( 'calypso_site_migration_automated_request_success' );
+ onSubmit();
+ }
+ }, [ isSuccess, onSubmit ] );
+
+ useEffect( () => {
+ if ( error ) {
+ recordTracksEvent( 'calypso_site_migration_automated_request_error' );
+ }
+ }, [ error ] );
+
useEffect( () => {
const { unsubscribe } = watch( () => {
clearErrors( 'root' );
@@ -117,8 +61,6 @@ export const useCredentialsForm = ( onSubmit: () => void ) => {
return () => unsubscribe();
}, [ watch, clearErrors ] );
- const accessMethod = watch( 'howToAccessSite' );
-
const submitHandler = ( data: CredentialsFormData ) => {
requestAutomatedMigration( data );
};
@@ -131,7 +73,6 @@ export const useCredentialsForm = ( onSubmit: () => void ) => {
accessMethod,
isPending,
submitHandler,
- setError,
importSiteQueryParam,
};
};
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-site-migration-credentials-mutation.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-site-migration-credentials-mutation.tsx
index 3ce58a8489c4b2..5fe17bc455d8bc 100644
--- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-site-migration-credentials-mutation.tsx
+++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/use-site-migration-credentials-mutation.tsx
@@ -1,7 +1,7 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
import wpcomRequest from 'wpcom-proxy-request';
import { useSiteSlugParam } from 'calypso/landing/stepper/hooks/use-site-slug-param';
-import { CredentialsFormData } from './types';
+import { ApiError, CredentialsFormData } from './types';
interface AutomatedMigrationAPIResponse {
success: boolean;
@@ -18,33 +18,32 @@ interface AutomatedMigrationBody {
}
export const useSiteMigrationCredentialsMutation = <
- TData = AutomatedMigrationAPIResponse | unknown,
- TError = unknown,
- TContext = unknown,
+ TData = AutomatedMigrationAPIResponse,
+ TError = ApiError,
>(
- options: UseMutationOptions< TData, TError, CredentialsFormData, TContext > = {}
+ options: UseMutationOptions< TData, TError, CredentialsFormData > = {}
) => {
const siteSlug = useSiteSlugParam();
- const { mutate, ...rest } = useMutation( {
+ return useMutation< TData, TError, CredentialsFormData >( {
mutationFn: ( {
- siteAddress,
+ from_url,
username,
password,
notes,
- howToAccessSite,
+ migrationType,
backupFileLocation,
- } ) => {
+ }: CredentialsFormData ) => {
let body: AutomatedMigrationBody = {
- migration_type: howToAccessSite,
+ migration_type: migrationType,
blog_url: siteSlug ?? '',
notes,
};
- if ( howToAccessSite === 'credentials' ) {
+ if ( migrationType === 'credentials' ) {
body = {
...body,
- from_url: siteAddress,
+ from_url,
username,
password,
};
@@ -66,9 +65,4 @@ export const useSiteMigrationCredentialsMutation = <
},
...options,
} );
-
- return {
- requestAutomatedMigration: mutate,
- ...rest,
- };
};