Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
acf4b32
fix: (CXSPA-9504) support Chinese address in address book
espada945 Mar 20, 2026
5768599
fix: (CXSPA-9504) update existing unit tests for new dependencies
espada945 Mar 23, 2026
1fa348c
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Mar 23, 2026
bb54f99
fix: add unit tests for China address logic to meet coverage threshold
espada945 Mar 23, 2026
8a57374
fix: format spec file with prettier
espada945 Mar 23, 2026
6055a54
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Mar 24, 2026
01fe95b
fix: add null fallback for async pipe in ng-select items
espada945 Mar 24, 2026
068cc2e
fix: (CXSPA-9504) address code review feedback for Chinese address su…
espada945 Mar 26, 2026
a512114
fix: (CXSPA-9504) disable city/district dropdowns until parent selected
espada945 Mar 26, 2026
9a5bd91
fix: (CXSPA-9504) add zh/zh_TW translations, fix tests and formatting
espada945 Mar 30, 2026
cb57b67
fix: (CXSPA-9504) format address-form component with prettier
espada945 Mar 30, 2026
17fb9be
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Mar 31, 2026
a823920
fix: (CXSPA-9504) reduce complexity in getCardContent, fix unnecessar…
espada945 Mar 31, 2026
5915982
fix: (CXSPA-9504) trigger CI retry
espada945 Mar 31, 2026
28c6b99
fix: (CXSPA-9504) trigger CI retry
espada945 Mar 31, 2026
1982b2b
fix: (CXSPA-9504) trigger CI retry
espada945 Mar 31, 2026
91a1228
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Apr 1, 2026
6a83f92
fix: (CXSPA-9504) use country isocode for address card display to mat…
espada945 Apr 1, 2026
becc9fe
fix: (CXSPA-9504) use region isocode instead of name in address card …
espada945 Apr 1, 2026
b1f7053
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Apr 7, 2026
d7248ae
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Apr 7, 2026
4ec0863
Merge branch 'develop' into bugfix/CXSPA-9504
i53577 Apr 7, 2026
6212cc8
refactor: (CXSPA-9504) move Chinese address API calls to UserProfileA…
espada945 Apr 9, 2026
c1129eb
fix: (CXSPA-9504) move getCities/getDistricts to UserAddressService v…
espada945 Apr 9, 2026
da98c45
fix: (CXSPA-9504) restore JSDoc comments in SiteAdapter
espada945 Apr 9, 2026
306ecd5
fix: (CXSPA-9504) fix CI failures - OPF spec missing SiteAdapter prov…
espada945 Apr 10, 2026
c90a80b
Merge branch 'develop' into bugfix/CXSPA-9504
espada945 Apr 10, 2026
4fd7273
fix: (CXSPA-9504) fix CI failures - OPF spec missing SiteAdapter prov…
espada945 Apr 10, 2026
e420c1d
fix: (CXSPA-9504) add SiteAdapter mock provider to UserAddressService…
espada945 Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"placeholder": "City"
},
"state": "State",
"province": "Province",
"district": "District",
"zipCode": {
"label": "Zip code",
"placeholder": "Postal Code/Zip"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,37 @@

<cx-form-required-asterisks />
</span>
<input
required="true"
type="text"
class="form-control"
placeholder="{{ 'addressForm.city.placeholder' | cxTranslate }}"
<ng-select
*ngIf="isChinaAddress; else cityTextInput"
class="city-select"
id="city-select"
formControlName="town"
[attr.aria-invalid]="
addressForm.get('town')?.touched && addressForm.get('town')?.invalid
"
[attr.aria-errormessage]="'townError'"
/>
[searchable]="true"
[clearable]="false"
[items]="(cities$ | async) ?? []"
bindLabel="name"
bindValue="isocode"
placeholder="{{ 'addressForm.selectOne' | cxTranslate }}"
(change)="citySelected($event)"
[cxNgSelectA11y]="{
ariaLabel: 'addressForm.city.label' | cxTranslate,
}"
>
</ng-select>
<ng-template #cityTextInput>
<input
required="true"
type="text"
class="form-control"
placeholder="{{ 'addressForm.city.placeholder' | cxTranslate }}"
formControlName="town"
[attr.aria-invalid]="
addressForm.get('town')?.touched &&
addressForm.get('town')?.invalid
"
[attr.aria-errormessage]="'townError'"
/>
</ng-template>
<cx-form-errors
id="townError"
[translationParams]="{
Expand Down Expand Up @@ -218,7 +238,11 @@
<div class="form-group col-md-6">
<label>
<span class="label-content required"
>{{ 'addressForm.state' | cxTranslate }}
>{{
isChinaAddress
? ('addressForm.province' | cxTranslate)
: ('addressForm.state' | cxTranslate)
}}
<cx-form-required-asterisks />
</span>
<ng-select
Expand All @@ -231,6 +255,7 @@
bindLabel="{{ regions[0].name ? 'name' : 'isocode' }}"
bindValue="{{ regions[0].name ? 'isocode' : 'region' }}"
placeholder="{{ 'addressForm.selectOne' | cxTranslate }}"
(change)="regionSelected($event)"
id="region-select"
ariaLabelDropdown="{{
'common.ngSelectDropdownOptionsList' | cxTranslate
Expand All @@ -247,6 +272,35 @@
</div>
</ng-container>
</ng-container>
<div class="form-group col-md-6" *ngIf="isChinaAddress">
<label>
<span class="label-content required"
>{{ 'addressForm.district' | cxTranslate }}
<cx-form-required-asterisks />
</span>
<ng-select
class="district-select"
id="district-select"
formControlName="district"
[searchable]="true"
[clearable]="false"
[items]="(districts$ | async) ?? []"
bindLabel="name"
bindValue="isocode"
placeholder="{{ 'addressForm.selectOne' | cxTranslate }}"
[cxNgSelectA11y]="{
ariaLabel: 'addressForm.district' | cxTranslate,
}"
>
</ng-select>
<cx-form-errors
[translationParams]="{
label: 'addressForm.district' | cxTranslate,
}"
[control]="addressForm.get('district')"
></cx-form-errors>
</label>
</div>
</div>

<div class="row">
Expand All @@ -267,15 +321,23 @@
</div>
<div class="form-group col-md-6">
<label>
<span class="label-content">{{
'addressForm.cellphone.label' | cxTranslate
}}</span>
<span class="label-content" [class.required]="isChinaAddress"
>{{ 'addressForm.cellphone.label' | cxTranslate }}
<cx-form-required-asterisks *ngIf="isChinaAddress" />
</span>
<input
type="tel"
class="form-control"
placeholder="{{ 'addressForm.cellphone.placeholder' | cxTranslate }}"
formControlName="cellphone"
/>
<cx-form-errors
*ngIf="isChinaAddress"
[translationParams]="{
label: 'addressForm.cellphone.label' | cxTranslate,
}"
[control]="addressForm.get('cellphone')"
></cx-form-errors>
</label>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HttpClient } from '@angular/common/http';
import {
ChangeDetectionStrategy,
DebugElement,
Expand All @@ -14,6 +15,7 @@ import {
Country,
GlobalMessageService,
I18nTestingModule,
OccEndpointsService,
Region,
Title,
UserAddressService,
Expand Down Expand Up @@ -70,6 +72,7 @@ const mockAddress: Address = {
line2: 'line2',
town: 'town',
region: { isocode: 'JP-27' },
district: '',
postalCode: 'zip',
country: { isocode: 'JP' },
phone: '123123123',
Expand Down Expand Up @@ -152,6 +155,14 @@ describe('AddressFormComponent', () => {
{ provide: UserAddressService, useClass: MockUserAddressService },
{ provide: GlobalMessageService, useValue: mockGlobalMessageService },
{ provide: UserProfileFacade, useClass: MockUserProfileFacade },
{
provide: HttpClient,
useValue: { get: () => EMPTY },
},
{
provide: OccEndpointsService,
useValue: { getBaseUrl: () => '' },
},
],
})
.overrideComponent(AddressFormComponent, {
Expand Down Expand Up @@ -361,6 +372,87 @@ describe('AddressFormComponent', () => {
);
});

it('should set isChinaAddress and add validators when CN is selected', () => {
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.countrySelected({ isocode: 'CN' });
expect(component.isChinaAddress).toBe(true);
expect(component.addressForm.get('cellphone')?.validator).toBeTruthy();
expect(component.addressForm.get('district')?.validator).toBeTruthy();
});

it('should clear validators when switching away from CN', () => {
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.countrySelected({ isocode: 'CN' });
component.countrySelected({ isocode: 'US' });
expect(component.isChinaAddress).toBe(false);
expect(component.addressForm.get('cellphone')?.validator).toBeNull();
expect(component.addressForm.get('district')?.validator).toBeNull();
});

it('should reset town and district when region changes for CN address', () => {
component.isChinaAddress = true;
component.addressForm.get('town')?.setValue('old-town');
component.addressForm.get('district')?.setValue('old-district');
component.regionSelected({ isocode: 'CN-11' });
expect(component.addressForm.get('town')?.value).toBeNull();
expect(component.addressForm.get('district')?.value).toBeNull();
});

it('should not reset town and district when region changes for non-CN address', () => {
component.isChinaAddress = false;
component.addressForm.get('town')?.setValue('old-town');
component.regionSelected({ isocode: 'US-CA' });
expect(component.addressForm.get('town')?.value).toEqual('old-town');
});

it('should update selectedCity$ and reset district on citySelected', () => {
component.addressForm.get('district')?.setValue('old-district');
component.citySelected({ isocode: 'CN-11-1' });
expect(component.addressForm.get('district')?.value).toBeNull();
});

it('should not update selectedCity$ when city has no isocode', () => {
component.addressForm.get('district')?.setValue('old-district');
component.citySelected({});
expect(component.addressForm.get('district')?.value).toEqual(
'old-district'
);
});

it('should set up cities$ observable in ngOnInit', () => {
spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([]));
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.ngOnInit();
expect(component.cities$).toBeDefined();
});

it('should set up districts$ observable in ngOnInit', () => {
spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([]));
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.ngOnInit();
expect(component.districts$).toBeDefined();
});

it('should return empty cities when no region is selected', (done) => {
spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([]));
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.ngOnInit();
component.cities$.pipe(take(1)).subscribe((cities) => {
expect(cities).toEqual([]);
done();
});
});

it('should return empty districts when no city is selected', (done) => {
spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([]));
spyOn(userAddressService, 'getRegions').and.returnValue(of([]));
component.ngOnInit();
component.districts$.pipe(take(1)).subscribe((districts) => {
expect(districts).toEqual([]);
done();
});
});

it('should call verifyAddress', () => {
spyOn(component, 'verifyAddress').and.callThrough();
const mockCountryIsocode = 'test country isocode';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { AsyncPipe, NgIf } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
TranslatePipe,
TranslationService,
UserAddressService,
OccEndpointsService,
} from '@spartacus/core';
import {
FormErrorsComponent,
Expand All @@ -47,7 +49,13 @@ import {
sortTitles,
} from '@spartacus/storefront';
import { UserProfileFacade } from '@spartacus/user/profile/root';
import { BehaviorSubject, Observable, Subscription, combineLatest } from 'rxjs';
import {
BehaviorSubject,
Observable,
Subscription,
combineLatest,
of,
} from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';

@Component({
Expand All @@ -72,6 +80,11 @@ export class AddressFormComponent implements OnInit, OnDestroy {
titles$: Observable<Title[]>;
regions$: Observable<Region[]>;
selectedCountry$: BehaviorSubject<string> = new BehaviorSubject<string>('');
selectedRegion$: BehaviorSubject<string> = new BehaviorSubject<string>('');
selectedCity$: BehaviorSubject<string> = new BehaviorSubject<string>('');
isChinaAddress = false;
cities$: Observable<any[]>;
districts$: Observable<any[]>;
addresses$: Observable<Address[]>;

@Input()
Expand Down Expand Up @@ -118,6 +131,7 @@ export class AddressFormComponent implements OnInit, OnDestroy {
region: this.fb.group({
isocode: [null, Validators.required],
}),
district: [''],
postalCode: ['', Validators.required],
phone: '',
cellphone: '',
Expand All @@ -130,7 +144,9 @@ export class AddressFormComponent implements OnInit, OnDestroy {
protected globalMessageService: GlobalMessageService,
protected translation: TranslationService,
protected launchDialogService: LaunchDialogService,
protected userProfileFacade: UserProfileFacade
protected userProfileFacade: UserProfileFacade,
protected http: HttpClient,
protected occEndpointsService: OccEndpointsService
) {}

ngOnInit() {
Expand Down Expand Up @@ -171,6 +187,34 @@ export class AddressFormComponent implements OnInit, OnDestroy {
}

this.addresses$ = this.userAddressService.getAddresses();

this.cities$ = this.selectedRegion$.pipe(
switchMap((regionIsocode) => {
if (!regionIsocode) {
return of([]);
}
const baseUrl = this.occEndpointsService.getBaseUrl();
return this.http
.get<any>(
`${baseUrl}/regions/${regionIsocode}/cities?fields=cities(name,isocode)`
)
.pipe(map((res) => res.cities ?? []));
})
);

this.districts$ = this.selectedCity$.pipe(
switchMap((cityIsocode) => {
if (!cityIsocode) {
return of([]);
}
const baseUrl = this.occEndpointsService.getBaseUrl();
return this.http
.get<any>(
`${baseUrl}/cities/${cityIsocode}/districts?fields=districts(name,isocode)`
)
.pipe(map((res) => res.districts ?? []));
})
);
}

getTitles(): Observable<Title[]> {
Expand Down Expand Up @@ -214,10 +258,37 @@ export class AddressFormComponent implements OnInit, OnDestroy {
countrySelected(country: Country | undefined): void {
this.addressForm.get('country')?.get('isocode')?.setValue(country?.isocode);
this.selectedCountry$.next(country?.isocode ?? '');
this.isChinaAddress = country?.isocode === 'CN';

const cellphoneControl = this.addressForm.get('cellphone');
const districtControl = this.addressForm.get('district');
if (this.isChinaAddress) {
cellphoneControl?.setValidators([Validators.required]);
districtControl?.setValidators([Validators.required]);
} else {
cellphoneControl?.clearValidators();
districtControl?.clearValidators();
districtControl?.reset();
}
cellphoneControl?.updateValueAndValidity();
districtControl?.updateValueAndValidity();
}

regionSelected(region: Region): void {
this.addressForm.get('region')?.get('isocode')?.setValue(region.isocode);
if (this.isChinaAddress) {
this.selectedRegion$.next(region.isocode ?? '');
this.addressForm.get('town')?.reset();
this.selectedCity$.next('');
this.addressForm.get('district')?.reset();
}
}

citySelected(city: any): void {
if (city?.isocode) {
this.selectedCity$.next(city.isocode);
this.addressForm.get('district')?.reset();
}
}

toggleDefaultAddress(): void {
Expand Down
Loading