File

feature-libs/checkout/components/components/payment-method/payment-form/payment-form.component.ts

Implements

OnInit

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector cx-payment-form
templateUrl ./payment-form.component.html

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(checkoutPaymentService: CheckoutPaymentFacade, checkoutDeliveryService: CheckoutDeliveryFacade, userPaymentService: UserPaymentService, globalMessageService: GlobalMessageService, fb: FormBuilder, modalService: ModalService, userAddressService: UserAddressService)
Parameters :
Name Type Optional
checkoutPaymentService CheckoutPaymentFacade No
checkoutDeliveryService CheckoutDeliveryFacade No
userPaymentService UserPaymentService No
globalMessageService GlobalMessageService No
fb FormBuilder No
modalService ModalService No
userAddressService UserAddressService No

Inputs

paymentMethodsCount
Type : number
setAsDefaultField
Type : boolean

Outputs

closeForm
Type : EventEmitter
goBack
Type : EventEmitter
setPaymentDetails
Type : EventEmitter

Methods

back
back()
Returns : void
close
close()
Returns : void
countrySelected
countrySelected(country: Country)
Parameters :
Name Type Optional
country Country No
Returns : void
expMonthAndYear
expMonthAndYear()
Returns : void
getAddressCardContent
getAddressCardContent(address: Address)
Parameters :
Name Type Optional
address Address No
Returns : Card
Protected handleAddressVerificationResults
handleAddressVerificationResults(results: AddressValidation)
Parameters :
Name Type Optional
results AddressValidation No
Returns : void
next
next()
Returns : void
ngOnInit
ngOnInit()
Returns : void
openSuggestedAddress
openSuggestedAddress(results: AddressValidation)
Parameters :
Name Type Optional
results AddressValidation No
Returns : void
toggleDefaultPaymentMethod
toggleDefaultPaymentMethod()
Returns : void
toggleSameAsShippingAddress
toggleSameAsShippingAddress()
Returns : void
verifyAddress
verifyAddress()
Returns : void

Properties

billingAddressForm
Type : FormGroup
Default value : this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], line1: ['', Validators.required], line2: [''], town: ['', Validators.required], region: this.fb.group({ isocodeShort: [null, Validators.required], }), country: this.fb.group({ isocode: [null, Validators.required], }), postalCode: ['', Validators.required], })
cardTypes$
Type : Observable<CardType[]>
closeForm
Default value : new EventEmitter<any>()
Decorators :
@Output()
countries$
Type : Observable<Country[]>
goBack
Default value : new EventEmitter<any>()
Decorators :
@Output()
iconTypes
Default value : ICON_TYPE
loading$
Type : Observable<StateUtils.LoaderState<void>>
months
Type : string[]
Default value : []
paymentForm
Type : FormGroup
Default value : this.fb.group({ cardType: this.fb.group({ code: [null, Validators.required], }), accountHolderName: ['', Validators.required], cardNumber: ['', Validators.required], expiryMonth: [null, Validators.required], expiryYear: [null, Validators.required], cvn: ['', Validators.required], defaultPayment: [false], })
paymentMethodsCount
Type : number
Decorators :
@Input()
regions$
Type : Observable<Region[]>
sameAsShippingAddress
Default value : true
selectedCountry$
Type : BehaviorSubject<string>
Default value : new BehaviorSubject<string>('')
setAsDefaultField
Type : boolean
Decorators :
@Input()
setPaymentDetails
Default value : new EventEmitter<any>()
Decorators :
@Output()
shippingAddress$
Type : Observable<Address>
showSameAsShippingAddressCheckbox$
Type : Observable<boolean>
suggestedAddressModalRef
Type : ModalRef | null
years
Type : number[]
Default value : []
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  OnInit,
  Output,
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {
  CheckoutDeliveryFacade,
  CheckoutPaymentFacade,
} from '@spartacus/checkout/root';
import {
  Address,
  AddressValidation,
  CardType,
  Country,
  GlobalMessageService,
  GlobalMessageType,
  Region,
  StateUtils,
  UserAddressService,
  UserPaymentService,
} from '@spartacus/core';
import {
  Card,
  ICON_TYPE,
  ModalRef,
  ModalService,
  SuggestedAddressDialogComponent,
} from '@spartacus/storefront';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';

@Component({
  selector: 'cx-payment-form',
  templateUrl: './payment-form.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentFormComponent implements OnInit {
  iconTypes = ICON_TYPE;

  suggestedAddressModalRef: ModalRef | null;
  months: string[] = [];
  years: number[] = [];

  cardTypes$: Observable<CardType[]>;
  shippingAddress$: Observable<Address>;
  countries$: Observable<Country[]>;
  loading$: Observable<StateUtils.LoaderState<void>>;
  sameAsShippingAddress = true;
  regions$: Observable<Region[]>;
  selectedCountry$: BehaviorSubject<string> = new BehaviorSubject<string>('');
  showSameAsShippingAddressCheckbox$: Observable<boolean>;

  @Input()
  setAsDefaultField: boolean;

  @Input()
  paymentMethodsCount: number;

  @Output()
  goBack = new EventEmitter<any>();

  @Output()
  closeForm = new EventEmitter<any>();

  @Output()
  setPaymentDetails = new EventEmitter<any>();

  paymentForm: FormGroup = this.fb.group({
    cardType: this.fb.group({
      code: [null, Validators.required],
    }),
    accountHolderName: ['', Validators.required],
    cardNumber: ['', Validators.required],
    expiryMonth: [null, Validators.required],
    expiryYear: [null, Validators.required],
    cvn: ['', Validators.required],
    defaultPayment: [false],
  });

  billingAddressForm: FormGroup = this.fb.group({
    firstName: ['', Validators.required],
    lastName: ['', Validators.required],
    line1: ['', Validators.required],
    line2: [''],
    town: ['', Validators.required],
    region: this.fb.group({
      isocodeShort: [null, Validators.required],
    }),
    country: this.fb.group({
      isocode: [null, Validators.required],
    }),
    postalCode: ['', Validators.required],
  });

  constructor(
    protected checkoutPaymentService: CheckoutPaymentFacade,
    protected checkoutDeliveryService: CheckoutDeliveryFacade,
    protected userPaymentService: UserPaymentService,
    protected globalMessageService: GlobalMessageService,
    protected fb: FormBuilder,
    protected modalService: ModalService,
    protected userAddressService: UserAddressService
  ) {}

  ngOnInit() {
    this.expMonthAndYear();
    this.countries$ = this.userPaymentService.getAllBillingCountries().pipe(
      tap((countries) => {
        // If the store is empty fetch countries. This is also used when changing language.
        if (Object.keys(countries).length === 0) {
          this.userPaymentService.loadBillingCountries();
        }
      })
    );

    this.cardTypes$ = this.checkoutPaymentService.getCardTypes().pipe(
      tap((cardTypes) => {
        if (Object.keys(cardTypes).length === 0) {
          this.checkoutPaymentService.loadSupportedCardTypes();
        }
      })
    );

    this.shippingAddress$ = this.checkoutDeliveryService.getDeliveryAddress();
    this.loading$ =
      this.checkoutPaymentService.getSetPaymentDetailsResultProcess();

    this.showSameAsShippingAddressCheckbox$ = combineLatest([
      this.countries$,
      this.shippingAddress$,
    ]).pipe(
      map(([countries, address]) => {
        return (
          (address?.country &&
            !!countries.filter(
              (country: Country): boolean =>
                country.isocode === address.country?.isocode
            ).length) ??
          false
        );
      }),
      tap((shouldShowCheckbox) => {
        this.sameAsShippingAddress = shouldShowCheckbox;
      })
    );

    this.regions$ = this.selectedCountry$.pipe(
      switchMap((country) => this.userAddressService.getRegions(country)),
      tap((regions) => {
        const regionControl = this.billingAddressForm.get(
          'region.isocodeShort'
        );
        if (regions.length > 0) {
          regionControl?.enable();
        } else {
          regionControl?.disable();
        }
      })
    );
  }

  expMonthAndYear(): void {
    const year = new Date().getFullYear();

    for (let i = 0; i < 10; i++) {
      this.years.push(year + i);
    }

    for (let j = 1; j <= 12; j++) {
      if (j < 10) {
        this.months.push(`0${j}`);
      } else {
        this.months.push(j.toString());
      }
    }
  }

  toggleDefaultPaymentMethod(): void {
    this.paymentForm.value.defaultPayment =
      !this.paymentForm.value.defaultPayment;
  }

  toggleSameAsShippingAddress(): void {
    this.sameAsShippingAddress = !this.sameAsShippingAddress;
  }

  getAddressCardContent(address: Address): Card {
    let region = '';
    if (address.region && address.region.isocode) {
      region = address.region.isocode + ', ';
    }

    return {
      textBold: address.firstName + ' ' + address.lastName,
      text: [
        address.line1,
        address.line2,
        address.town + ', ' + region + address.country?.isocode,
        address.postalCode,
        address.phone,
      ],
    } as Card;
  }

  openSuggestedAddress(results: AddressValidation): void {
    if (!this.suggestedAddressModalRef) {
      this.suggestedAddressModalRef = this.modalService.open(
        SuggestedAddressDialogComponent,
        { centered: true, size: 'lg' }
      );
      this.suggestedAddressModalRef.componentInstance.enteredAddress =
        this.billingAddressForm.value;
      this.suggestedAddressModalRef.componentInstance.suggestedAddresses =
        results.suggestedAddresses;
      this.suggestedAddressModalRef.result
        .then(() => {
          this.suggestedAddressModalRef = null;
        })
        .catch(() => {
          // this  callback is called when modal is closed with Esc key or clicking backdrop
          this.suggestedAddressModalRef = null;
        });
    }
  }

  close(): void {
    this.closeForm.emit();
  }

  back(): void {
    this.goBack.emit();
  }

  verifyAddress(): void {
    if (this.sameAsShippingAddress) {
      this.next();
    } else {
      this.userAddressService
        .verifyAddress(this.billingAddressForm.value)
        .subscribe((result) => {
          this.handleAddressVerificationResults(result);
        });
    }
  }

  protected handleAddressVerificationResults(results: AddressValidation) {
    if (results.decision === 'ACCEPT') {
      this.next();
    } else if (results.decision === 'REJECT') {
      this.globalMessageService.add(
        { key: 'addressForm.invalidAddress' },
        GlobalMessageType.MSG_TYPE_ERROR
      );
    } else if (results.decision === 'REVIEW') {
      this.openSuggestedAddress(results);
    }
  }

  countrySelected(country: Country): void {
    this.billingAddressForm.get('country.isocode')?.setValue(country.isocode);
    this.selectedCountry$.next(country.isocode as string);
  }

  next(): void {
    if (this.paymentForm.valid) {
      if (this.sameAsShippingAddress) {
        this.setPaymentDetails.emit({
          paymentDetails: this.paymentForm.value,
          billingAddress: null,
        });
      } else {
        if (this.billingAddressForm.valid) {
          this.setPaymentDetails.emit({
            paymentDetails: this.paymentForm.value,
            billingAddress: this.billingAddressForm.value,
          });
        } else {
          this.billingAddressForm.markAllAsTouched();
        }
      }
    } else {
      this.paymentForm.markAllAsTouched();

      if (!this.sameAsShippingAddress) {
        this.billingAddressForm.markAllAsTouched();
      }
    }
  }
}
<!-- FORM -->
<ng-container *ngIf="!(loading$ | async)?.loading; else spinner">
  <form (ngSubmit)="next()" [formGroup]="paymentForm">
    <div class="row">
      <div class="col-md-12 col-xl-10">
        <div class="form-group" formGroupName="cardType">
          <ng-container *ngIf="cardTypes$ | async as cardTypes">
            <div *ngIf="cardTypes.length !== 0">
              <label>
                <span class="label-content required">{{
                  'paymentForm.paymentType' | cxTranslate
                }}</span>
                <ng-select
                  aria-required="true"
                  [searchable]="true"
                  [clearable]="false"
                  [items]="cardTypes"
                  bindLabel="name"
                  bindValue="code"
                  placeholder="{{ 'paymentForm.selectOne' | cxTranslate }}"
                  formControlName="code"
                >
                </ng-select>
                <cx-form-errors
                  aria-live="assertive"
                  aria-atomic="true"
                  [control]="paymentForm.get('cardType.code')"
                ></cx-form-errors>
              </label>
            </div>
          </ng-container>
        </div>

        <div class="form-group">
          <label>
            <span class="label-content">{{
              'paymentForm.accountHolderName.label' | cxTranslate
            }}</span>
            <input
              aria-required="true"
              class="form-control"
              type="text"
              placeholder="{{
                'paymentForm.accountHolderName.placeholder' | cxTranslate
              }}"
              formControlName="accountHolderName"
            />
            <cx-form-errors
              aria-live="assertive"
              aria-atomic="true"
              [control]="paymentForm.get('accountHolderName')"
            ></cx-form-errors>
          </label>
        </div>

        <div class="form-group">
          <label>
            <span class="label-content">{{
              'paymentForm.cardNumber' | cxTranslate
            }}</span>
            <input
              aria-required="true"
              type="text"
              class="form-control"
              formControlName="cardNumber"
            />
            <cx-form-errors
              aria-live="assertive"
              aria-atomic="true"
              [control]="paymentForm.get('cardNumber')"
            ></cx-form-errors>
          </label>
        </div>

        <div class="row">
          <div class="form-group col-md-8">
            <fieldset class="cx-payment-form-exp-date">
              <legend class="label-content">
                {{ 'paymentForm.expirationDate' | cxTranslate }}
              </legend>
              <label class="cx-payment-form-exp-date-wrapper">
                <ng-select
                  aria-required="true"
                  [searchable]="true"
                  [clearable]="false"
                  [items]="months"
                  placeholder="{{ 'paymentForm.monthMask' | cxTranslate }}"
                  formControlName="expiryMonth"
                  [attr.aria-label]="
                    'paymentForm.expirationMonth'
                      | cxTranslate
                        : { selected: paymentForm.get('expiryMonth')?.value }
                  "
                >
                </ng-select>
                <cx-form-errors
                  aria-live="assertive"
                  aria-atomic="true"
                  [control]="paymentForm.get('expiryMonth')"
                ></cx-form-errors>
              </label>
              <label class="cx-payment-form-exp-date-wrapper">
                <ng-select
                  aria-required="true"
                  [searchable]="true"
                  [clearable]="false"
                  [items]="years"
                  placeholder="{{ 'paymentForm.yearMask' | cxTranslate }}"
                  [attr.aria-label]="
                    'paymentForm.expirationYear'
                      | cxTranslate
                        : { selected: paymentForm.get('expiryYear')?.value }
                  "
                  formControlName="expiryYear"
                >
                </ng-select>
                <cx-form-errors
                  aria-live="assertive"
                  aria-atomic="true"
                  [control]="paymentForm.get('expiryYear')"
                ></cx-form-errors>
              </label>
            </fieldset>
          </div>

          <div class="form-group col-md-4">
            <label>
              <span class="label-content">
                {{ 'paymentForm.securityCode' | cxTranslate }}
                <cx-icon
                  [type]="iconTypes.INFO"
                  class="cx-payment-form-tooltip"
                  placement="right"
                  title="{{ 'paymentForm.securityCodeTitle' | cxTranslate }}"
                  alt=""
                ></cx-icon>
              </span>
              <input
                aria-required="true"
                type="text"
                class="form-control"
                id="cVVNumber"
                formControlName="cvn"
              />
              <cx-form-errors
                aria-live="assertive"
                aria-atomic="true"
                [control]="paymentForm.get('cvn')"
              ></cx-form-errors>
            </label>
          </div>
        </div>

        <div class="form-group" *ngIf="setAsDefaultField">
          <div class="form-check">
            <label>
              <input
                type="checkbox"
                class="form-check-input"
                (change)="toggleDefaultPaymentMethod()"
              />
              <span class="form-check-label">{{
                'paymentForm.setAsDefault' | cxTranslate
              }}</span>
            </label>
          </div>
        </div>

        <!-- BILLING -->
        <div class="cx-payment-form-billing">
          <div class="cx-payment-form-billing-address">
            {{ 'paymentForm.billingAddress' | cxTranslate }}
          </div>

          <!-- SAME AS SHIPPING CHECKBOX -->
          <ng-container *ngIf="showSameAsShippingAddressCheckbox$ | async">
            <div class="form-group">
              <div class="form-check">
                <label>
                  <input
                    type="checkbox"
                    class="form-check-input"
                    [checked]="sameAsShippingAddress"
                    (change)="toggleSameAsShippingAddress()"
                  />
                  <span class="form-check-label">{{
                    'paymentForm.sameAsShippingAddress' | cxTranslate
                  }}</span>
                </label>
              </div>
            </div>
          </ng-container>

          <!-- BILLING INFO COMPONENT -->
          <ng-container
            *ngIf="
              sameAsShippingAddress &&
                (shippingAddress$ | async) as shippingAddress;
              else billingAddress
            "
          >
            <cx-card
              [content]="getAddressCardContent(shippingAddress)"
            ></cx-card>
          </ng-container>

          <ng-template #billingAddress>
            <div [formGroup]="billingAddressForm">
              <div class="form-group" formGroupName="country">
                <ng-container *ngIf="countries$ | async as countries">
                  <div *ngIf="countries.length !== 0">
                    <label aria-required="true">
                      <span class="label-content required">{{
                        'addressForm.country' | cxTranslate
                      }}</span>
                      <ng-select
                        aria-required="true"
                        [searchable]="true"
                        [clearable]="false"
                        [items]="countries"
                        bindLabel="name"
                        bindValue="isocode"
                        placeholder="{{
                          'addressForm.selectOne' | cxTranslate
                        }}"
                        (change)="countrySelected($event)"
                        formControlName="isocode"
                      >
                      </ng-select>
                      <cx-form-errors
                        aria-live="assertive"
                        aria-atomic="true"
                        [control]="billingAddressForm.get('country.isocode')"
                      ></cx-form-errors>
                    </label>
                  </div>
                </ng-container>
              </div>
              <div class="form-group">
                <label>
                  <span class="label-content required">{{
                    'addressForm.firstName.label' | cxTranslate
                  }}</span>
                  <input
                    aria-required="true"
                    class="form-control"
                    type="text"
                    placeholder="{{
                      'addressForm.firstName.placeholder' | cxTranslate
                    }}"
                    formControlName="firstName"
                  />
                  <cx-form-errors
                    aria-live="assertive"
                    aria-atomic="true"
                    [control]="billingAddressForm.get('firstName')"
                  ></cx-form-errors>
                </label>
              </div>
              <div class="form-group">
                <label>
                  <span class="label-content required">{{
                    'addressForm.lastName.label' | cxTranslate
                  }}</span>
                  <input
                    aria-required="true"
                    type="text"
                    class="form-control"
                    placeholder="{{
                      'addressForm.lastName.placeholder' | cxTranslate
                    }}"
                    formControlName="lastName"
                  />
                  <cx-form-errors
                    aria-live="assertive"
                    aria-atomic="true"
                    [control]="billingAddressForm.get('lastName')"
                  ></cx-form-errors>
                </label>
              </div>
              <div class="form-group">
                <label>
                  <span class="label-content required">{{
                    'addressForm.address1' | cxTranslate
                  }}</span>
                  <input
                    aria-required="true"
                    type="text"
                    class="form-control"
                    placeholder="{{
                      'addressForm.streetAddress' | cxTranslate
                    }}"
                    formControlName="line1"
                  />
                  <cx-form-errors
                    aria-live="assertive"
                    aria-atomic="true"
                    [control]="billingAddressForm.get('line1')"
                  ></cx-form-errors>
                </label>
              </div>
              <div class="form-group">
                <label>
                  <span class="label-content">{{
                    'addressForm.address2' | cxTranslate
                  }}</span>
                  <input
                    type="text"
                    class="form-control"
                    placeholder="{{ 'addressForm.aptSuite' | cxTranslate }}"
                    formControlName="line2"
                  />
                </label>
              </div>
              <div class="row">
                <div class="form-group col-md-6">
                  <label>
                    <span class="label-content required">{{
                      'addressForm.city.label' | cxTranslate
                    }}</span>
                    <input
                      aria-required="true"
                      type="text"
                      class="form-control"
                      placeholder="{{
                        'addressForm.city.placeholder' | cxTranslate
                      }}"
                      formControlName="town"
                    />
                    <cx-form-errors
                      aria-live="assertive"
                      aria-atomic="true"
                      [control]="billingAddressForm.get('town')"
                    ></cx-form-errors>
                  </label>
                </div>
                <div class="form-group col-md-6">
                  <label>
                    <span class="label-content required">{{
                      'addressForm.zipCode.label' | cxTranslate
                    }}</span>
                    <input
                      aria-required="true"
                      type="text"
                      class="form-control"
                      placeholder="{{
                        'addressForm.zipCode.placeholder' | cxTranslate
                      }}"
                      formControlName="postalCode"
                    />
                    <cx-form-errors
                      aria-live="assertive"
                      aria-atomic="true"
                      [control]="billingAddressForm.get('postalCode')"
                    ></cx-form-errors>
                  </label>
                </div>
                <ng-container
                  *ngIf="regions$ | async as regions"
                  formGroupName="region"
                >
                  <ng-container *ngIf="regions.length !== 0">
                    <div class="form-group col-md-6">
                      <label aria-required="true">
                        <span class="label-content required">{{
                          'addressForm.state' | cxTranslate
                        }}</span>
                        <ng-select
                          aria-required="true"
                          class="region-select"
                          formControlName="isocodeShort"
                          [searchable]="true"
                          [clearable]="false"
                          [items]="regions"
                          bindLabel="{{
                            regions[0].name ? 'name' : 'isocodeShort'
                          }}"
                          bindValue="{{
                            regions[0].name ? 'isocodeShort' : 'region'
                          }}"
                          placeholder="{{
                            'addressForm.selectOne' | cxTranslate
                          }}"
                        >
                        </ng-select>
                        <cx-form-errors
                          aria-live="assertive"
                          aria-atomic="true"
                          [control]="
                            billingAddressForm.get('region.isocodeShort')
                          "
                        ></cx-form-errors>
                      </label>
                    </div>
                  </ng-container>
                </ng-container>
              </div>
            </div>
          </ng-template>
        </div>
      </div>
    </div>

    <!-- BUTTON SECTION -->
    <div class="cx-checkout-btns row">
      <div class="col-md-12 col-lg-6">
        <button
          *ngIf="paymentMethodsCount === 0"
          class="btn btn-block btn-action"
          (click)="back()"
        >
          {{ 'common.back' | cxTranslate }}
        </button>
        <button
          *ngIf="paymentMethodsCount > 0"
          class="btn btn-block btn-action"
          (click)="close()"
        >
          {{ 'paymentForm.changePayment' | cxTranslate }}
        </button>
      </div>
      <div class="col-md-12 col-lg-6">
        <button class="btn btn-block btn-primary" type="submit">
          {{ 'common.continue' | cxTranslate }}
        </button>
      </div>
    </div>
  </form>
</ng-container>

<ng-template #spinner>
  <cx-spinner></cx-spinner>
</ng-template>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""