feature-libs/checkout/components/components/payment-method/payment-form/payment-form.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | cx-payment-form |
| templateUrl | ./payment-form.component.html |
Properties |
Methods |
Inputs |
Outputs |
constructor(checkoutPaymentService: CheckoutPaymentFacade, checkoutDeliveryService: CheckoutDeliveryFacade, userPaymentService: UserPaymentService, globalMessageService: GlobalMessageService, fb: FormBuilder, modalService: ModalService, userAddressService: UserAddressService)
|
||||||||||||||||||||||||
|
Parameters :
|
| paymentMethodsCount | |
Type : number
|
|
| setAsDefaultField | |
Type : boolean
|
|
| closeForm | |
Type : EventEmitter
|
|
| goBack | |
Type : EventEmitter
|
|
| setPaymentDetails | |
Type : EventEmitter
|
|
| back |
back()
|
|
Returns :
void
|
| close |
close()
|
|
Returns :
void
|
| countrySelected | ||||||
countrySelected(country: Country)
|
||||||
|
Parameters :
Returns :
void
|
| expMonthAndYear |
expMonthAndYear()
|
|
Returns :
void
|
| getAddressCardContent | ||||||
getAddressCardContent(address: Address)
|
||||||
|
Parameters :
Returns :
Card
|
| Protected handleAddressVerificationResults | ||||||
handleAddressVerificationResults(results: AddressValidation)
|
||||||
|
Parameters :
Returns :
void
|
| next |
next()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Returns :
void
|
| openSuggestedAddress | ||||||
openSuggestedAddress(results: AddressValidation)
|
||||||
|
Parameters :
Returns :
void
|
| toggleDefaultPaymentMethod |
toggleDefaultPaymentMethod()
|
|
Returns :
void
|
| toggleSameAsShippingAddress |
toggleSameAsShippingAddress()
|
|
Returns :
void
|
| verifyAddress |
verifyAddress()
|
|
Returns :
void
|
| 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>