Delayed Account Creation: Support option to send password setup link to customer via email (#51227)

* Update form handling to work with automatically generated passwords

* Keep user logged in after password reset.

* Ignore comments for hooks in changed method

* Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce

* Checkmark list style

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Mike Jolley 2024-09-19 15:03:27 +01:00 committed by GitHub
parent c5ed90535f
commit d567c6c698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 149 additions and 75 deletions

View File

@ -32,7 +32,9 @@ const defaultTemplate = [
], ],
[ [
'core/list', 'core/list',
{}, {
className: 'is-style-checkmark-list',
},
[ [
[ [
'core/list-item', 'core/list-item',

View File

@ -9,6 +9,7 @@ import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings';
import { ValidatedTextInput } from '@woocommerce/blocks-components'; import { ValidatedTextInput } from '@woocommerce/blocks-components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { getSetting } from '@woocommerce/settings';
const termsPageLink = TERMS_URL ? ( const termsPageLink = TERMS_URL ? (
<a href={ TERMS_URL } target="_blank" rel="noreferrer"> <a href={ TERMS_URL } target="_blank" rel="noreferrer">
@ -26,6 +27,67 @@ const privacyPageLink = PRIVACY_URL ? (
<span>{ __( 'Privacy Policy', 'woocommerce' ) }</span> <span>{ __( 'Privacy Policy', 'woocommerce' ) }</span>
); );
const PasswordField = ( {
isLoading,
password,
setPassword,
}: {
isLoading: boolean;
password: string;
setPassword: ( password: string ) => void;
} ) => {
const [ passwordStrength, setPasswordStrength ] = useState( 0 );
return (
<div>
<ValidatedTextInput
disabled={ isLoading }
type="password"
label={ __( 'Password', 'woocommerce' ) }
className={ `wc-block-components-address-form__password` }
value={ password }
required={ true }
errorId={ 'account-password' }
customValidityMessage={ (
validity: ValidityState
): string | undefined => {
if (
validity.valueMissing ||
validity.badInput ||
validity.typeMismatch
) {
return __(
'Please enter a valid password',
'woocommerce'
);
}
} }
customValidation={ ( inputObject ) => {
if ( passwordStrength < 2 ) {
inputObject.setCustomValidity(
__(
'Please create a stronger password',
'woocommerce'
)
);
return false;
}
return true;
} }
onChange={ ( value: string ) => setPassword( value ) }
feedback={
<PasswordStrengthMeter
password={ password }
onChange={ ( strength: number ) =>
setPasswordStrength( strength )
}
/>
}
/>
</div>
);
};
const Form = ( { const Form = ( {
attributes: blockAttributes, attributes: blockAttributes,
isEditor, isEditor,
@ -35,7 +97,6 @@ const Form = ( {
} ) => { } ) => {
const [ isLoading, setIsLoading ] = useState( false ); const [ isLoading, setIsLoading ] = useState( false );
const [ password, setPassword ] = useState( '' ); const [ password, setPassword ] = useState( '' );
const [ passwordStrength, setPasswordStrength ] = useState( 0 );
const hasValidationError = useSelect( ( select ) => const hasValidationError = useSelect( ( select ) =>
select( VALIDATION_STORE_KEY ).getValidationError( 'account-password' ) select( VALIDATION_STORE_KEY ).getValidationError( 'account-password' )
); );
@ -44,6 +105,13 @@ const Form = ( {
( isEditor ? 'customer@email.com' : '' ); ( isEditor ? 'customer@email.com' : '' );
const nonceToken = blockAttributes?.nonceToken || ''; const nonceToken = blockAttributes?.nonceToken || '';
// Passwords might not be required based on settings.
const registrationGeneratePassword = getSetting(
'registrationGeneratePassword',
false
);
const needsPassword = ! registrationGeneratePassword && ! password;
return ( return (
<form <form
className={ 'wc-block-order-confirmation-create-account-form' } className={ 'wc-block-order-confirmation-create-account-form' }
@ -58,66 +126,29 @@ const Form = ( {
setIsLoading( true ); setIsLoading( true );
} } } }
> >
<p> { ! registrationGeneratePassword && (
{ createInterpolateElement( <>
__( 'Set a password for <email/>', 'woocommerce' ), <p>
{ { createInterpolateElement(
email: <strong>{ customerEmail }</strong>, __( 'Set a password for <email/>', 'woocommerce' ),
} {
) } email: <strong>{ customerEmail }</strong>,
</p>
<div>
<ValidatedTextInput
disabled={ isLoading }
type="password"
label={ __( 'Password', 'woocommerce' ) }
className={ `wc-block-components-address-form__password` }
value={ password }
required={ true }
errorId={ 'account-password' }
customValidityMessage={ (
validity: ValidityState
): string | undefined => {
if (
validity.valueMissing ||
validity.badInput ||
validity.typeMismatch
) {
return __(
'Please enter a valid password',
'woocommerce'
);
}
} }
customValidation={ ( inputObject ) => {
if ( passwordStrength < 2 ) {
inputObject.setCustomValidity(
__(
'Please create a stronger password',
'woocommerce'
)
);
return false;
}
return true;
} }
onChange={ ( value: string ) => setPassword( value ) }
feedback={
<PasswordStrengthMeter
password={ password }
onChange={ ( strength: number ) =>
setPasswordStrength( strength )
} }
/> ) }
} </p>
/> <PasswordField
</div> isLoading={ isLoading }
password={ password }
setPassword={ setPassword }
/>
</>
) }
<Button <Button
className={ className={
'wc-block-order-confirmation-create-account-button' 'wc-block-order-confirmation-create-account-button'
} }
type="submit" type="submit"
disabled={ !! hasValidationError || ! password || isLoading } disabled={ !! hasValidationError || needsPassword || isLoading }
showSpinner={ isLoading } showSpinner={ isLoading }
> >
{ __( 'Create account', 'woocommerce' ) } { __( 'Create account', 'woocommerce' ) }
@ -126,16 +157,26 @@ const Form = ( {
<input type="hidden" name="password" value={ password } /> <input type="hidden" name="password" value={ password } />
<input type="hidden" name="create-account" value="1" /> <input type="hidden" name="create-account" value="1" />
<input type="hidden" name="_wpnonce" value={ nonceToken } /> <input type="hidden" name="_wpnonce" value={ nonceToken } />
<p className={ 'wc-block-order-confirmation-create-account-terms' }> <div className="wc-block-order-confirmation-create-account-description">
{ createInterpolateElement( { registrationGeneratePassword && (
/* translators: %1$s terms page link, %2$s privacy page link. */ <p>
__( { __(
'By creating an account you agree to our <terms/> and <privacy/>.', "We'll email you a link to set up an account password.",
'woocommerce' 'woocommerce'
), ) }
{ terms: termsPageLink, privacy: privacyPageLink } </p>
) } ) }
</p> <p>
{ createInterpolateElement(
/* translators: %1$s terms page link, %2$s privacy page link. */
__(
'By creating an account you agree to our <terms/> and <privacy/>.',
'woocommerce'
),
{ terms: termsPageLink, privacy: privacyPageLink }
) }
</p>
</div>
</form> </form>
); );
}; };

View File

@ -53,13 +53,15 @@
padding: 1em; padding: 1em;
} }
.wc-block-order-confirmation-create-account-terms { .wc-block-order-confirmation-create-account-description {
@include font-size(small); p {
text-align: center; @include font-size(small);
text-align: center;
a, a,
span { span {
white-space: nowrap; white-space: nowrap;
}
} }
} }

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Delayed account creation: Support option to send password setup link to customer via email.

View File

@ -358,16 +358,21 @@ class WC_Shortcode_My_Account {
/** /**
* Handles resetting the user's password. * Handles resetting the user's password.
* *
* @since 9.4.0 This will log the user in after resetting the password/session.
*
* @param object $user The user. * @param object $user The user.
* @param string $new_pass New password for the user in plaintext. * @param string $new_pass New password for the user in plaintext.
*/ */
public static function reset_password( $user, $new_pass ) { public static function reset_password( $user, $new_pass ) {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
do_action( 'password_reset', $user, $new_pass ); do_action( 'password_reset', $user, $new_pass );
wp_set_password( $new_pass, $user->ID ); wp_set_password( $new_pass, $user->ID );
update_user_meta( $user->ID, 'default_password_nag', false ); update_user_meta( $user->ID, 'default_password_nag', false );
self::set_reset_password_cookie(); self::set_reset_password_cookie();
wc_set_customer_auth_cookie( $user->ID );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) { if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) {
wp_password_change_notification( $user ); wp_password_change_notification( $user );
} }

View File

@ -40,7 +40,7 @@ class CreateAccount extends AbstractOrderConfirmationBlock {
* @return \WP_Error|int * @return \WP_Error|int
*/ */
protected function process_form_post( $order ) { protected function process_form_post( $order ) {
if ( ! isset( $_POST['create-account'], $_POST['email'], $_POST['password'], $_POST['_wpnonce'] ) ) { if ( ! isset( $_POST['create-account'], $_POST['email'], $_POST['_wpnonce'] ) ) {
return 0; return 0;
} }
@ -49,7 +49,6 @@ class CreateAccount extends AbstractOrderConfirmationBlock {
} }
$user_email = sanitize_email( wp_unslash( $_POST['email'] ) ); $user_email = sanitize_email( wp_unslash( $_POST['email'] ) );
$password = wp_unslash( $_POST['password'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Does order already have user? // Does order already have user?
if ( $order->get_customer_id() ) { if ( $order->get_customer_id() ) {
@ -61,8 +60,16 @@ class CreateAccount extends AbstractOrderConfirmationBlock {
return new \WP_Error( 'email_mismatch', __( 'The email address provided does not match the email address on this order.', 'woocommerce' ) ); return new \WP_Error( 'email_mismatch', __( 'The email address provided does not match the email address on this order.', 'woocommerce' ) );
} }
if ( empty( $password ) || strlen( $password ) < 8 ) { $generate_password = get_option( 'woocommerce_registration_generate_password', false );
return new \WP_Error( 'password_too_short', __( 'Password must be at least 8 characters.', 'woocommerce' ) );
if ( $generate_password ) {
$password = ''; // Will be generated by wc_create_new_customer.
} else {
$password = wp_unslash( $_POST['password'] ?? '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( empty( $password ) || strlen( $password ) < 8 ) {
return new \WP_Error( 'password_too_short', __( 'Password must be at least 8 characters.', 'woocommerce' ) );
}
} }
$customer_id = wc_create_new_customer( $customer_id = wc_create_new_customer(
@ -171,4 +178,17 @@ class CreateAccount extends AbstractOrderConfirmationBlock {
return $content; return $content;
} }
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'registrationGeneratePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) );
}
} }