Checkout: Add password strength meter to new field (#49164)
* Add validation message and fix rendering when account is required * Adjust validation so we can change the label in messages with custom callback * Add and style password meter * Add validation feedback and zxcvbn support * changelog * Accessibility fixes * Update lock file * Fix initial state in safari * Announce strength as you type * Use React.ReactElement * update lock file * Update lock * Downgrade local pnpm and recreate lock * Feedback should only be shown when `showError` is true, not `hasError` * Feedback should default to null
This commit is contained in:
parent
320873a4dc
commit
77428b973d
|
@ -11,18 +11,21 @@ import { ContactFormValues, getSetting } from '@woocommerce/settings';
|
||||||
import {
|
import {
|
||||||
StoreNoticesContainer,
|
StoreNoticesContainer,
|
||||||
CheckboxControl,
|
CheckboxControl,
|
||||||
ValidatedTextInput,
|
|
||||||
} from '@woocommerce/blocks-components';
|
} from '@woocommerce/blocks-components';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import { CONTACT_FORM_KEYS } from '@woocommerce/block-settings';
|
import { CONTACT_FORM_KEYS } from '@woocommerce/block-settings';
|
||||||
import { Form } from '@woocommerce/base-components/cart-checkout';
|
import { Form } from '@woocommerce/base-components/cart-checkout';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import CreatePassword from './create-password';
|
||||||
|
|
||||||
const CreateAccountUI = (): React.ReactElement | null => {
|
const CreateAccountUI = (): React.ReactElement | null => {
|
||||||
const { customerPassword, shouldCreateAccount } = useSelect( ( select ) => {
|
const { shouldCreateAccount } = useSelect( ( select ) => {
|
||||||
const store = select( CHECKOUT_STORE_KEY );
|
const store = select( CHECKOUT_STORE_KEY );
|
||||||
return {
|
return {
|
||||||
customerPassword: store.getCustomerPassword(),
|
|
||||||
shouldCreateAccount: store.getShouldCreateAccount(),
|
shouldCreateAccount: store.getShouldCreateAccount(),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
@ -72,32 +75,7 @@ const CreateAccountUI = (): React.ReactElement | null => {
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
{ showCreateAccountPassword && (
|
{ showCreateAccountPassword && <CreatePassword /> }
|
||||||
<ValidatedTextInput
|
|
||||||
type="password"
|
|
||||||
label={ __( 'Create a password', 'woocommerce' ) }
|
|
||||||
className={ `wc-block-components-address-form__password` }
|
|
||||||
value={ customerPassword }
|
|
||||||
required={ true }
|
|
||||||
customValidityMessage={ (
|
|
||||||
validity: ValidityState
|
|
||||||
): string | undefined => {
|
|
||||||
if (
|
|
||||||
validity.valueMissing ||
|
|
||||||
validity.badInput ||
|
|
||||||
validity.typeMismatch
|
|
||||||
) {
|
|
||||||
return __(
|
|
||||||
'Please enter a valid password',
|
|
||||||
'woocommerce'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
onChange={ ( value: string ) =>
|
|
||||||
__internalSetCustomerPassword( value )
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import PasswordStrengthMeter from '../../password-strength-meter';
|
||||||
|
|
||||||
|
const CreatePassword = () => {
|
||||||
|
const [ passwordStrength, setPasswordStrength ] = useState( 0 );
|
||||||
|
const { customerPassword } = useSelect( ( select ) => {
|
||||||
|
const store = select( CHECKOUT_STORE_KEY );
|
||||||
|
return {
|
||||||
|
customerPassword: store.getCustomerPassword(),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
const { __internalSetCustomerPassword } = useDispatch( CHECKOUT_STORE_KEY );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ValidatedTextInput
|
||||||
|
type="password"
|
||||||
|
label={ __( 'Create a password', 'woocommerce' ) }
|
||||||
|
className={ `wc-block-components-address-form__password` }
|
||||||
|
value={ customerPassword }
|
||||||
|
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 ) =>
|
||||||
|
__internalSetCustomerPassword( value )
|
||||||
|
}
|
||||||
|
feedback={
|
||||||
|
<PasswordStrengthMeter
|
||||||
|
password={ customerPassword }
|
||||||
|
onChange={ ( strength: number ) =>
|
||||||
|
setPasswordStrength( strength )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreatePassword;
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { sprintf, __ } from '@wordpress/i18n';
|
||||||
|
import { useInstanceId } from '@wordpress/compose';
|
||||||
|
import { passwordStrength } from 'check-password-strength';
|
||||||
|
import { usePrevious } from '@woocommerce/base-hooks';
|
||||||
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
zxcvbn: ( password: string ) => {
|
||||||
|
score: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoreDescriptions = [
|
||||||
|
__( 'Too weak', 'woocommerce' ),
|
||||||
|
__( 'Weak', 'woocommerce' ),
|
||||||
|
__( 'Medium', 'woocommerce' ),
|
||||||
|
__( 'Strong', 'woocommerce' ),
|
||||||
|
__( 'Very strong', 'woocommerce' ),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a password strength meter.
|
||||||
|
*
|
||||||
|
* Uses zxcvbn to calculate the password strength if available, otherwise falls back to check-password-strength which
|
||||||
|
* does not include dictionaries of common passwords.
|
||||||
|
*/
|
||||||
|
const PasswordStrengthMeter = ( {
|
||||||
|
password = '',
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
password: string;
|
||||||
|
onChange?: ( strength: number ) => void;
|
||||||
|
} ): React.ReactElement | null => {
|
||||||
|
const instanceId = useInstanceId(
|
||||||
|
PasswordStrengthMeter,
|
||||||
|
'woocommerce-password-strength-meter'
|
||||||
|
) as string;
|
||||||
|
|
||||||
|
let strength = -1;
|
||||||
|
|
||||||
|
if ( password.length > 0 ) {
|
||||||
|
if ( typeof window.zxcvbn === 'undefined' ) {
|
||||||
|
const result = passwordStrength( password );
|
||||||
|
strength = result.id;
|
||||||
|
} else {
|
||||||
|
const result = window.zxcvbn( password );
|
||||||
|
strength = result.score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStrength = usePrevious( strength );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( strength !== previousStrength && onChange ) {
|
||||||
|
onChange( strength );
|
||||||
|
}
|
||||||
|
}, [ strength, previousStrength, onChange ] );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={ instanceId }
|
||||||
|
className={ clsx( 'wc-block-components-password-strength', {
|
||||||
|
hidden: strength === -1,
|
||||||
|
} ) }
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor={ instanceId + '-meter' }
|
||||||
|
className="screen-reader-text"
|
||||||
|
>
|
||||||
|
{ __( 'Password strength', 'woocommerce' ) }
|
||||||
|
</label>
|
||||||
|
<meter
|
||||||
|
id={ instanceId + '-meter' }
|
||||||
|
className="wc-block-components-password-strength__meter"
|
||||||
|
min={ 0 }
|
||||||
|
max={ 4 }
|
||||||
|
value={ strength > -1 ? strength : 0 }
|
||||||
|
>
|
||||||
|
{ scoreDescriptions[ strength ] ?? '' }
|
||||||
|
</meter>
|
||||||
|
<div
|
||||||
|
id={ instanceId + '-result' }
|
||||||
|
className="wc-block-components-password-strength__result"
|
||||||
|
>
|
||||||
|
{ !! scoreDescriptions[ strength ] && (
|
||||||
|
<>
|
||||||
|
<span className="screen-reader-text" aria-live="polite">
|
||||||
|
{ sprintf(
|
||||||
|
/* translators: %s: Password strength */
|
||||||
|
__(
|
||||||
|
'Password strength: %1$s (%2$d characters long)',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
scoreDescriptions[ strength ],
|
||||||
|
password.length
|
||||||
|
) }
|
||||||
|
</span>{ ' ' }
|
||||||
|
<span aria-hidden={ true }>
|
||||||
|
{ scoreDescriptions[ strength ] }
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordStrengthMeter;
|
|
@ -0,0 +1,73 @@
|
||||||
|
.wc-block-components-password-strength {
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__meter {
|
||||||
|
margin: $gap-small 0 0;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 0;
|
||||||
|
background-color: $universal-border-light;
|
||||||
|
color: $alert-red;
|
||||||
|
|
||||||
|
&::-webkit-meter-bar,
|
||||||
|
&::-webkit-meter-inner-element {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
height: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
&::-webkit-meter-optimum-value,
|
||||||
|
&::-webkit-meter-even-less-good-value,
|
||||||
|
&::-webkit-meter-suboptimum-value {
|
||||||
|
background: none;
|
||||||
|
background-color: currentColor;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 0;
|
||||||
|
height: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:-moz-meter-optimum::-moz-meter-bar,
|
||||||
|
&:-moz-meter-sub-optimum::-moz-meter-bar,
|
||||||
|
&:-moz-meter-sub-sub-optimum::-moz-meter-bar {
|
||||||
|
background: none;
|
||||||
|
background-color: currentColor;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 0;
|
||||||
|
height: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__result {
|
||||||
|
@include font-size(smaller);
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
margin: $gap-smallest 0 0;
|
||||||
|
color: $alert-red;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "\00a0 ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__meter[value="1"],
|
||||||
|
.wc-block-components-password-strength__meter[value="1"] + .wc-block-components-password-strength__result {
|
||||||
|
color: $alert-red;
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__meter[value="2"],
|
||||||
|
.wc-block-components-password-strength__meter[value="2"] + .wc-block-components-password-strength__result {
|
||||||
|
color: #ff6f00;
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__meter[value="3"],
|
||||||
|
.wc-block-components-password-strength__meter[value="3"] + .wc-block-components-password-strength__result {
|
||||||
|
color: $alert-yellow;
|
||||||
|
}
|
||||||
|
.wc-block-components-password-strength__meter[value="4"],
|
||||||
|
.wc-block-components-password-strength__meter[value="4"] + .wc-block-components-password-strength__result {
|
||||||
|
color: $alert-green;
|
||||||
|
}
|
||||||
|
}
|
|
@ -275,6 +275,7 @@
|
||||||
"@wordpress/url": "3.13.0",
|
"@wordpress/url": "3.13.0",
|
||||||
"@wordpress/wordcount": "3.47.0",
|
"@wordpress/wordcount": "3.47.0",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
|
"check-password-strength": "^2.0.10",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"compare-versions": "4.1.3",
|
"compare-versions": "4.1.3",
|
||||||
"config": "3.3.7",
|
"config": "3.3.7",
|
||||||
|
|
|
@ -34,7 +34,7 @@ const defaultValidityMessage =
|
||||||
const getValidityMessageForInput = (
|
const getValidityMessageForInput = (
|
||||||
label: string | undefined,
|
label: string | undefined,
|
||||||
inputElement: HTMLInputElement,
|
inputElement: HTMLInputElement,
|
||||||
customValidityMessage?: ( validity: ValidityState ) => string
|
customValidityMessage?: ( validity: ValidityState ) => string | undefined
|
||||||
): string => {
|
): string => {
|
||||||
// No errors, or custom error - return early.
|
// No errors, or custom error - return early.
|
||||||
if ( inputElement.validity.valid || inputElement.validity.customError ) {
|
if ( inputElement.validity.valid || inputElement.validity.customError ) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ export interface ValidatedTextInputProps
|
||||||
ariaDescribedBy?: string | undefined;
|
ariaDescribedBy?: string | undefined;
|
||||||
// id to use for the error message. If not provided, an id will be generated.
|
// id to use for the error message. If not provided, an id will be generated.
|
||||||
errorId?: string;
|
errorId?: string;
|
||||||
|
// Feedback to display alongside the input. May be hidden when validation errors are displayed.
|
||||||
|
feedback?: JSX.Element | null;
|
||||||
// if true, the input will be focused on mount.
|
// if true, the input will be focused on mount.
|
||||||
focusOnMount?: boolean;
|
focusOnMount?: boolean;
|
||||||
// Callback to run on change which is passed the updated value.
|
// Callback to run on change which is passed the updated value.
|
||||||
|
@ -37,9 +39,7 @@ export interface ValidatedTextInputProps
|
||||||
| ( ( inputObject: HTMLInputElement ) => boolean )
|
| ( ( inputObject: HTMLInputElement ) => boolean )
|
||||||
| undefined;
|
| undefined;
|
||||||
// Custom validation message to display when validity is false. Given the input element. Expected to use inputObject.validity.
|
// Custom validation message to display when validity is false. Given the input element. Expected to use inputObject.validity.
|
||||||
customValidityMessage?:
|
customValidityMessage?: ( validity: ValidityState ) => undefined | string;
|
||||||
| ( ( validity: ValidityState ) => undefined | string )
|
|
||||||
| undefined;
|
|
||||||
// Custom formatted to format values as they are typed.
|
// Custom formatted to format values as they are typed.
|
||||||
customFormatter?: ( value: string ) => string;
|
customFormatter?: ( value: string ) => string;
|
||||||
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
|
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
|
||||||
|
|
|
@ -50,6 +50,7 @@ const ValidatedTextInput = forwardRef<
|
||||||
value = '',
|
value = '',
|
||||||
customValidation = () => true,
|
customValidation = () => true,
|
||||||
customValidityMessage,
|
customValidityMessage,
|
||||||
|
feedback = null,
|
||||||
customFormatter = ( newValue: string ) => newValue,
|
customFormatter = ( newValue: string ) => newValue,
|
||||||
label,
|
label,
|
||||||
validateOnMount = true,
|
validateOnMount = true,
|
||||||
|
@ -241,12 +242,14 @@ const ValidatedTextInput = forwardRef<
|
||||||
id={ textInputId }
|
id={ textInputId }
|
||||||
type={ type }
|
type={ type }
|
||||||
feedback={
|
feedback={
|
||||||
showError ? (
|
showError && hasError ? (
|
||||||
<ValidationInputError
|
<ValidationInputError
|
||||||
errorMessage={ passedErrorMessage }
|
errorMessage={ passedErrorMessage }
|
||||||
propertyName={ errorIdString }
|
propertyName={ errorIdString }
|
||||||
/>
|
/>
|
||||||
) : null
|
) : (
|
||||||
|
feedback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ref={ inputRef }
|
ref={ inputRef }
|
||||||
onChange={ ( newValue ) => {
|
onChange={ ( newValue ) => {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Add strength meter to block checkout password field.
|
|
@ -94,10 +94,17 @@ class Checkout extends AbstractBlock {
|
||||||
* @return array|string
|
* @return array|string
|
||||||
*/
|
*/
|
||||||
protected function get_block_type_script( $key = null ) {
|
protected function get_block_type_script( $key = null ) {
|
||||||
|
$dependencies = [];
|
||||||
|
|
||||||
|
// Load password strength meter script asynchronously if needed.
|
||||||
|
if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
|
||||||
|
$dependencies[] = 'zxcvbn-async';
|
||||||
|
}
|
||||||
|
|
||||||
$script = [
|
$script = [
|
||||||
'handle' => 'wc-' . $this->block_name . '-block-frontend',
|
'handle' => 'wc-' . $this->block_name . '-block-frontend',
|
||||||
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
|
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
|
||||||
'dependencies' => [],
|
'dependencies' => $dependencies,
|
||||||
];
|
];
|
||||||
return $key ? $script[ $key ] : $script;
|
return $key ? $script[ $key ] : $script;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3928,6 +3928,9 @@ importers:
|
||||||
change-case:
|
change-case:
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
|
check-password-strength:
|
||||||
|
specifier: ^2.0.10
|
||||||
|
version: 2.0.10
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
@ -13254,6 +13257,9 @@ packages:
|
||||||
engines: {node: '>=8.3.0'}
|
engines: {node: '>=8.3.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
check-password-strength@2.0.10:
|
||||||
|
resolution: {integrity: sha512-HRM5ICPmtnNtLnTv2QrfVkq1IxI9z3bzYpDJ1k5ixwD9HtJGHuv265R6JmHOV6r8wLhQMlULnIUVpkrC2yaiCw==}
|
||||||
|
|
||||||
check-types@8.0.3:
|
check-types@8.0.3:
|
||||||
resolution: {integrity: sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==}
|
resolution: {integrity: sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==}
|
||||||
|
|
||||||
|
@ -43968,7 +43974,7 @@ snapshots:
|
||||||
'@babel/code-frame': 7.23.5
|
'@babel/code-frame': 7.23.5
|
||||||
'@babel/parser': 7.23.5
|
'@babel/parser': 7.23.5
|
||||||
'@babel/traverse': 7.23.5
|
'@babel/traverse': 7.23.5
|
||||||
'@babel/types': 7.24.7
|
'@babel/types': 7.23.5
|
||||||
eslint: 7.32.0
|
eslint: 7.32.0
|
||||||
eslint-visitor-keys: 1.3.0
|
eslint-visitor-keys: 1.3.0
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
|
@ -45585,6 +45591,8 @@ snapshots:
|
||||||
run-parallel: 1.2.0
|
run-parallel: 1.2.0
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
|
check-password-strength@2.0.10: {}
|
||||||
|
|
||||||
check-types@8.0.3: {}
|
check-types@8.0.3: {}
|
||||||
|
|
||||||
checkstyle-formatter@1.1.0:
|
checkstyle-formatter@1.1.0:
|
||||||
|
|
Loading…
Reference in New Issue