Skip to content

Commit

Permalink
Create the SITE_MIGRATION_SECURE_CREDENTIALS step
Browse files Browse the repository at this point in the history
  • Loading branch information
andregardi committed Nov 29, 2024
1 parent 61cc12b commit e4a500e
Show file tree
Hide file tree
Showing 12 changed files with 809 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const useCredentialsForm = (
onSubmit( siteInfoResult );
} else {
const applicationPasswordsInfo = {
isAvailable: true,
isAvailable: false,
};
onSubmit( siteInfoResult, applicationPasswordsInfo );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const getAction = ( siteInfo?: UrlData, applicationPasswordsInfo?: ApplicationPa
return 'application-passwords-approval';
}

if ( applicationPasswordsInfo?.isAvailable === false ) {
return 'credentials-required';
}

if ( siteInfo?.platform_data?.is_wpcom ) {
return 'already-wpcom';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,15 +639,15 @@ describe( 'SiteMigrationCredentials', () => {
} );
} );

it( 'submits application-passwords-approval action when using password application', async () => {
it( 'submits credentials-required action when using password application', async () => {
const submit = jest.fn();
render( { navigation: { submit } } );
await fillAddressField();
( wp.req.get as jest.Mock ).mockResolvedValue( baseSiteInfo );
await userEvent.click( continueButton() );

expect( submit ).toHaveBeenCalledWith( {
action: 'application-passwords-approval',
action: 'credentials-required',
from: 'https://site-url.wordpress.com',
platform: 'wordpress',
} );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { NextButton } from '@automattic/onboarding';
import { useTranslate } from 'i18n-calypso';
import { FC } from 'react';
import Notice from 'calypso/components/notice';
import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
import { ErrorMessage } from '../../site-migration-credentials/components/error-message';
import { PasswordField } from '../../site-migration-credentials/components/password-field';
import { SpecialInstructions } from '../../site-migration-credentials/components/special-instructions';
import { UsernameField } from '../../site-migration-credentials/components/username-field';
import { useCredentialsForm } from '../hooks/use-credentials-form';

interface CredentialsFormProps {
onSubmit: ( from?: string ) => void;
onSkip: () => void;
}

export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip } ) => {
const translate = useTranslate();
const { control, errors, isBusy, submitHandler, canBypassVerification } =
useCredentialsForm( onSubmit );

const queryError = useQuery().get( 'error' ) || null;

let errorMessage;
if ( errors.root && errors.root.type !== 'manual' && errors.root.message ) {
errorMessage = errors.root.message;
} else if ( queryError === 'ticket-creation' ) {
errorMessage = translate(
'We ran into a problem submitting your details. Please try again shortly.'
);
}

const getContinueButtonText = () => {
if ( isBusy && ! canBypassVerification ) {
return translate( 'Verifying credentials' );
}
if ( canBypassVerification ) {
return translate( 'Continue anyways' );
}

return translate( 'Continue' );
};

return (
<form className="site-migration-credentials__form" onSubmit={ submitHandler }>
{ errorMessage && (
<Notice
className="site-migration-credentials__error-notice"
status="is-warning"
showDismiss={ false }
>
{ errorMessage }
</Notice>
) }
<div className="site-migration-credentials__content">
<div className="site-migration-credentials">
<UsernameField control={ control } errors={ errors } />
<PasswordField control={ control } errors={ errors } />
</div>

<SpecialInstructions control={ control } errors={ errors } />

<ErrorMessage
error={ errors.root && errors.root.type === 'manual' ? errors.root : undefined }
/>

<div className="site-migration-credentials__submit">
<NextButton disabled={ isBusy } type="submit">
{ getContinueButtonText() }
</NextButton>
</div>
</div>

<div className="site-migration-credentials__skip">
<button
className="button navigation-link step-container__navigation-link has-underline is-borderless"
onClick={ onSkip }
type="button"
>
{ translate( 'I need help, please contact me' ) }
</button>
</div>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
import { useSiteSlugParam } from 'calypso/landing/stepper/hooks/use-site-slug-param';
import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
import { CredentialsFormData } from '../../site-migration-credentials/types';
import { useFormErrorMapping } from './use-form-error-mapping';
import { useRequestAutomatedMigration } from './use-request-automated-migration';

export const useCredentialsForm = ( onSubmit: ( from?: string ) => void ) => {
const siteSlug = useSiteSlugParam();
const importSiteQueryParam = useQuery().get( 'from' ) || '';
const [ isBusy, setIsBusy ] = useState( false );

const {
mutateAsync: requestAutomatedMigration,
error,
variables,
reset,
} = useRequestAutomatedMigration( siteSlug );

const serverSideError = useFormErrorMapping( error, variables );

const {
formState: { errors, isSubmitting },
control,
handleSubmit,
watch,
clearErrors,
} = useForm< CredentialsFormData >( {
mode: 'onSubmit',
reValidateMode: 'onSubmit',
disabled: isBusy,
defaultValues: {
from_url: importSiteQueryParam,
username: '',
password: '',
notes: '',
},
errors: serverSideError,
} );

useEffect( () => {
setIsBusy( isSubmitting );
}, [ isSubmitting ] );

const isLoginFailed =
error?.code === 'automated_migration_tools_login_and_get_cookies_test_failed';

const submitHandler = handleSubmit( async ( data: CredentialsFormData ) => {
clearErrors();

try {
const payload = {
...data,
bypassVerification: isLoginFailed,
};
await requestAutomatedMigration( payload );
recordTracksEvent( 'calypso_site_migration_automated_request_success' );
onSubmit( importSiteQueryParam );
} catch ( error ) {
recordTracksEvent( 'calypso_site_migration_automated_request_error' );
}
} );

useEffect( () => {
const { unsubscribe } = watch( () => {
clearErrors( 'root' );
reset();
} );
return () => unsubscribe();
}, [ watch, clearErrors, reset ] );

return {
errors,
control,
handleSubmit,
submitHandler,
isBusy,
canBypassVerification: isLoginFailed,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useTranslate } from 'i18n-calypso';
import { useCallback, useMemo } from 'react';
import { FieldErrors } from 'react-hook-form';
import { ApiError, CredentialsFormData } from '../../site-migration-credentials/types';

// ** 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(
() => ( {
username: { type: 'manual', message: translate( 'Enter a valid username.' ) },
password: { type: 'manual', message: translate( 'Enter a valid password.' ) },
} ),
[ translate ]
);

const credentialsErrorMessage = useMemo( () => {
return {
username: {
type: 'manual',
message: translate( 'Check your username.' ),
},
password: {
type: 'manual',
message: translate( 'Check your password.' ),
},
};
}, [ 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 ) => {
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 === 'automated_migration_tools_login_and_get_cookies_test_failed' ) {
return {
root: {
type: 'special',
message: translate(
'We could not verify your credentials. Can you double check your account information and try again?'
),
},
...credentialsErrorMessage,
};
}

if ( code !== 'rest_invalid_param' || ! data?.params ) {
return { root: { type: 'manual', message } };
}

const invalidFields = Object.keys( data.params );

return invalidFields.reduce(
( errors, key ) => {
const message = getTranslatedMessage( key );

errors[ key ] = message;
return errors;
},
{} as Record< string, { type: string; message: string } >
);
},
[ getTranslatedMessage, translate, credentialsErrorMessage ]
);

return useMemo( () => {
const serverError = error && variables ? handleServerError( error ) : undefined;

if ( serverError ) {
return {
...( serverError || {} ),
};
}

return undefined;
}, [ error, handleServerError, variables ] );
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useLocale } from '@automattic/i18n-utils';
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
import wpcomRequest from 'wpcom-proxy-request';
import { ApiError, CredentialsFormData } from '../../site-migration-credentials/types';

interface AutomatedMigrationAPIResponse {
success: boolean;
}

interface AutomatedMigration {
migration_type: 'credentials';
blog_url: string;
from_url?: string;
username?: string;
password?: string;
notes?: string;
bypass_verification?: boolean;
}

const requestAutomatedMigration = async (
siteSlug: string,
payload: AutomatedMigration,
locale: string
): Promise< AutomatedMigrationAPIResponse > => {
return wpcomRequest( {
path: `sites/${ siteSlug }/automated-migration?_locale=${ locale }`,
apiNamespace: 'wpcom/v2/',
apiVersion: '2',
method: 'POST',
body: payload,
} );
};

export const useRequestAutomatedMigration = (
siteSlug?: string | null,
options: UseMutationOptions< AutomatedMigrationAPIResponse, ApiError, CredentialsFormData > = {}
) => {
const locale = useLocale();
return useMutation< AutomatedMigrationAPIResponse, ApiError, CredentialsFormData >( {
mutationFn: ( { from_url, username, password, notes, bypassVerification } ) => {
if ( ! siteSlug ) {
throw new Error( 'Site slug is required' );
}

const body: AutomatedMigration = {
migration_type: 'credentials',
blog_url: siteSlug ?? '',
notes,
from_url,
username,
password,
bypass_verification: bypassVerification,
};

return requestAutomatedMigration( siteSlug, body, locale );
},
...options,
} );
};
Loading

0 comments on commit e4a500e

Please sign in to comment.