Skip to content

Commit

Permalink
AD-224 Implement Robust Form Validation for Delivery and Billing Addr…
Browse files Browse the repository at this point in the history
…ess Forms
  • Loading branch information
pjaneta committed Apr 5, 2024
1 parent 5ce94dc commit 67a641b
Show file tree
Hide file tree
Showing 21 changed files with 214 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.adyen.commerce.controllers.api;

import com.adyen.commerce.exceptions.AdyenControllerException;

import com.adyen.commerce.response.ErrorResponse;
import de.hybris.platform.acceleratorstorefrontcommons.annotations.RequireHardLogIn;
import de.hybris.platform.acceleratorstorefrontcommons.forms.AddressForm;
import de.hybris.platform.acceleratorstorefrontcommons.forms.validation.AddressValidator;
Expand All @@ -20,12 +18,15 @@
import org.springframework.stereotype.Controller;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;

import static com.adyen.commerce.util.FieldValidationUtil.getFieldCodesFromValidation;

@Controller
@RequestMapping(value = "/api/account")
public class AdyenAddressController {
Expand Down Expand Up @@ -62,7 +63,7 @@ public ResponseEntity<Void> addDeliveryAddress(@RequestBody AddressForm addressF
final Errors errors = new BeanPropertyBindingResult(addressForm, "address");
addressValidator.validate(addressForm, errors);
if (errors.hasErrors()) {
throw new AdyenControllerException("checkout.deliveryAddress.notSelected");
throw new AdyenControllerException("checkout.deliveryAddress.notSelected", getFieldCodesFromValidation(errors));
}

AddressData addressData = addressDataUtil.convertToAddressData(addressForm);
Expand Down Expand Up @@ -95,7 +96,7 @@ public ResponseEntity<Void> updateDeliveryAddress(@RequestBody AddressForm addre
final Errors errors = new BeanPropertyBindingResult(addressForm, "address");
addressValidator.validate(addressForm, errors);
if (errors.hasErrors()) {
throw new AdyenControllerException("checkout.deliveryAddress.notSelected");
throw new AdyenControllerException("checkout.deliveryAddress.notSelected", getFieldCodesFromValidation(errors));
}

AddressData addressData = addressDataUtil.convertToAddressData(addressForm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static com.adyen.commerce.util.ErrorMessageUtil.getErrorMessageByRefusalReason;


import static com.adyen.commerce.util.FieldValidationUtil.getFieldCodesFromValidation;
import static com.adyen.model.checkout.PaymentResponse.ResultCodeEnum.CHALLENGESHOPPER;
import static com.adyen.model.checkout.PaymentResponse.ResultCodeEnum.IDENTIFYSHOPPER;
import static com.adyen.model.checkout.PaymentResponse.ResultCodeEnum.REDIRECTSHOPPER;
Expand Down Expand Up @@ -80,12 +81,7 @@ public class AdyenPlaceOrderController {
@PostMapping("/place-order")
public ResponseEntity<PlaceOrderResponse> placeOrder(@RequestBody AdyenPaymentForm adyenPaymentForm, HttpServletRequest request) throws Exception {

final boolean selectPaymentMethodSuccess = selectPaymentMethod(adyenPaymentForm);

if (!selectPaymentMethodSuccess) {
LOGGER.warn("Payment form is invalid.");
throw new AdyenControllerException(CHECKOUT_ERROR_FORM_ENTRY_INVALID);
}
selectPaymentMethod(adyenPaymentForm);

if (!isCartValid()) {
LOGGER.warn("Cart is invalid.");
Expand Down Expand Up @@ -232,16 +228,16 @@ private ResponseEntity<PlaceOrderResponse> handleOther(HttpServletRequest reques
throw new AdyenControllerException(errorMessage);
}

private boolean selectPaymentMethod(AdyenPaymentForm adyenPaymentForm) {
private void selectPaymentMethod(AdyenPaymentForm adyenPaymentForm) {
final BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(adyenPaymentForm, "payment");
adyenCheckoutFacade.handlePaymentForm(adyenPaymentForm, bindingResult);


if (bindingResult.hasErrors()) {
LOGGER.warn(bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getCode).reduce((x, y) -> (x = x + y)));
return false;
LOGGER.warn("Payment form is invalid.");
LOGGER.warn(bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getCode).reduce((x, y) -> (x + " " + y)));
throw new AdyenControllerException(CHECKOUT_ERROR_FORM_ENTRY_INVALID, getFieldCodesFromValidation(bindingResult));
}
return true;
}

private ResponseEntity<PlaceOrderResponse> handleRedirect(String adyenPaymentMethod, PaymentResponse paymentResponse) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.adyen.commerce.util;

import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;

import java.util.List;

public class FieldValidationUtil {
public static List<String> getFieldCodesFromValidation(Errors errors) {
return errors.getFieldErrors().stream().map(FieldError::getField).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {translationsStore} from "../../store/translationsStore";
interface Props {
addressConfig: AddressConfigModel,
address: AddressModel,
errorFieldCodes: string[]
errorFieldCodePrefix: string
onCountryCodeChange: (countryCode: string) => void,
onTitleCodeChange: (titleCode: string) => void,
onFirstNameChange: (firstName: string) => void,
Expand All @@ -24,39 +26,64 @@ export class AddressForm extends React.Component<Props, any> {
render() {
return <>
<InputDropdown testId={"address.country"}
values={this.props.addressConfig.countries} fieldName={translationsStore.get("address.country")}
values={this.props.addressConfig.countries}
fieldName={translationsStore.get("address.country")}
onChange={(countryCode) => this.props.onCountryCodeChange(countryCode)}
selectedValue={this.props.address.countryCode}
placeholderText={translationsStore.get("address.selectCountry")} placeholderDisabled={true}/>
placeholderText={translationsStore.get("address.selectCountry")} placeholderDisabled={true}
fieldErrorId={this.props.errorFieldCodePrefix + "countryIso"}
fieldErrorTextCode="address.country.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputDropdown testId={"address.title"}
values={this.props.addressConfig.titles} fieldName={translationsStore.get("address.title")}
onChange={(titleCode) => this.props.onTitleCodeChange(titleCode)}
selectedValue={this.props.address.titleCode}
placeholderText={translationsStore.get("address.title.none")}/>
placeholderText={translationsStore.get("address.title.none")}
fieldErrorId={this.props.errorFieldCodePrefix + "titleCode"}
fieldErrorTextCode="address.title.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.firstName"}
fieldName={translationsStore.get("address.firstName")}
onChange={(firstName) => this.props.onFirstNameChange(firstName)}
value={this.props.address.firstName}/>
value={this.props.address.firstName}
fieldErrorId={this.props.errorFieldCodePrefix + "firstName"}
fieldErrorTextCode="address.firstName.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.surname"}
fieldName={translationsStore.get("address.surname")}
onChange={(lastName) => this.props.onLastNameChange(lastName)}
value={this.props.address.lastName}/>
value={this.props.address.lastName}
fieldErrorId={this.props.errorFieldCodePrefix + "lastName"}
fieldErrorTextCode="address.lastName.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.line1"}
fieldName={translationsStore.get("address.line1")}
onChange={(line1) => this.props.onLine1Change(line1)}
value={this.props.address.line1}/>
value={this.props.address.line1}
fieldErrorId={this.props.errorFieldCodePrefix + "line1"}
fieldErrorTextCode="address.line1.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.line2"}
fieldName={translationsStore.get("address.line2")}
onChange={(line2) => this.props.onLine2Change(line2)}
value={this.props.address.line2}/>
value={this.props.address.line2}
fieldErrorId={this.props.errorFieldCodePrefix + "line2"}
fieldErrorTextCode="address.line2.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.townCity"}
fieldName={translationsStore.get("address.townCity")}
onChange={(city) => this.props.onCityChange(city)}
value={this.props.address.city}/>
value={this.props.address.city}
fieldErrorId={this.props.errorFieldCodePrefix + "townCity"}
fieldErrorTextCode="address.townCity.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.postcode"}
fieldName={translationsStore.get("address.postcode")}
onChange={(postCode) => this.props.onPostCodeChange(postCode)}
value={this.props.address.postalCode}/>
value={this.props.address.postalCode}
fieldErrorId={this.props.errorFieldCodePrefix + "postcode"}
fieldErrorTextCode="address.postcode.invalid"
fieldErrors={this.props.errorFieldCodes}/>
<InputText testId={"address.phone"}
fieldName={translationsStore.get("address.phone")}
onChange={(phoneNumber) => this.props.onPhoneNumberChange(phoneNumber)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface ComponentProps {
addressConfig: AddressConfigModel
addressBook: AddressModel[]
saveInAddressBook: boolean
errorFieldCodes: string[]
errorFieldCodePrefix: string

onSelectAddress: (address: AddressModel) => void

Expand Down Expand Up @@ -98,6 +100,8 @@ class AddressSection extends React.Component<Props, State> {
<AddressForm addressConfig={this.props.addressConfig}
onCountryCodeChange={(countryCode) => this.props.onCountryCodeChange(countryCode)}
address={this.props.address}
errorFieldCodes={this.props.errorFieldCodes}
errorFieldCodePrefix={this.props.errorFieldCodePrefix}
onTitleCodeChange={(titleCode) => this.props.onTitleCodeChange(titleCode)}
onFirstNameChange={(firstName) => this.props.onFirstNameChange(firstName)}
onLastNameChange={(lastName) => this.props.onLastNameChange(lastName)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import React, {RefObject} from "react";
import React from "react";


export class ScrollHere extends React.Component<{}, null> {
private readonly ref: RefObject<any> = undefined;
private scrollHereClass: string = "adyen-scroll-here";

constructor(props: {}) {
super(props);
this.ref = React.createRef();
private scrollToFirstInstance() {
let elementsByClassName = document.getElementsByClassName(this.scrollHereClass);
if (elementsByClassName && elementsByClassName.length > 0) {
let newTop = this.getNewTopPosition(elementsByClassName);
newTop = newTop - 10;
window.scrollBy({top: newTop, behavior: 'smooth'});
}
}

private getNewTopPosition(elementsByClassName: HTMLCollectionOf<Element>) {
let newTop: number = undefined;

for (let i = 0; i < elementsByClassName.length; i++) {
let elementTop = elementsByClassName.item(i).getBoundingClientRect().top;
if (i === 0) {
newTop = elementTop;
}
if (elementTop < newTop) {
newTop = elementTop;
}
}
return newTop;
}

componentDidMount() {
if (this.ref) {
this.ref.current.scrollIntoView({behavior: 'smooth'})
}
this.scrollToFirstInstance()
}

render() {
return <div ref={this.ref}></div>
return <div className={this.scrollHereClass}></div>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import {translationsStore} from "../../store/translationsStore";

interface ComponentProps {
hasError: boolean
errorMessageCode: string
}

type Props = React.PropsWithChildren & ComponentProps

export class FieldGroup extends React.Component<Props, null> {

render() {
if (this.props.hasError) {
return (
<div className={"form-group has-error"}>
{this.props.children}
<div className="help-block">
<span>{translationsStore.get(this.props.errorMessageCode)}</span>
</div>
</div>
)
} else {
return (
<div className={"form-group"}>
{this.props.children}
</div>
)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import {CodeValueItem} from "../../reducers/types";
import {isNotEmpty} from "../../util/stringUtil";
import {FieldGroup} from "./FieldGroup";

interface InputDropdownProps {
testId?: string
Expand All @@ -9,6 +10,9 @@ interface InputDropdownProps {
selectedValue?: string
placeholderText?: string
placeholderDisabled?: boolean
fieldErrorId?: string
fieldErrorTextCode?: string
fieldErrors?: string[]
onChange?: (value: string) => void
}

Expand Down Expand Up @@ -59,15 +63,22 @@ export class InputDropdown extends React.Component<InputDropdownProps, null> {
} else {
return <></>
}
}

private hasError(): boolean {
if (!this.props.fieldErrors) {
return false;
}

return this.props.fieldErrors.includes(this.props.fieldErrorId);
}

render() {
return (
<div className={"form-group"}>
<FieldGroup errorMessageCode={this.props.fieldErrorTextCode} hasError={this.hasError()}>
{this.renderLabel()}
{this.renderInput()}
</div>
</FieldGroup>
)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import React from "react";
import {isNotEmpty} from "../../util/stringUtil";
import {FieldGroup} from "./FieldGroup";


interface InputTextProps {
testId?: string
fieldName: string
value?: string
fieldErrorId?: string
fieldErrorTextCode?: string
fieldErrors?: string[]
onChange: (value: string) => void
}

export class InputText extends React.Component<InputTextProps, null> {

private hasError(): boolean {
if (!this.props.fieldErrors) {
return false;
}

return this.props.fieldErrors.includes(this.props.fieldErrorId);
}

private renderInput(): React.JSX.Element {
if (isNotEmpty(this.props.testId)) {
return <input id={this.props.testId}
Expand All @@ -25,10 +37,11 @@ export class InputText extends React.Component<InputTextProps, null> {

render() {
return (
<div className={"form-group"}>
<FieldGroup errorMessageCode={this.props.fieldErrorTextCode} hasError={this.hasError()}>
<label className={"form-input_name control-label"}>{this.props.fieldName}</label>
{this.renderInput()}
</div>
</FieldGroup>

)
}
}
Loading

0 comments on commit 67a641b

Please sign in to comment.