From 7f6b77d6b9f50d45e4e3278341584ed00db83eb7 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Mon, 9 Dec 2024 18:30:36 +0800 Subject: [PATCH 01/15] CXSPA-9098, fix the issue Coupon Codes is used in Coupon Campaign URL for SPA UI --- .../assets/src/translations/en/myAccount.json | 7 + .../adapters/user/default-occ-user-config.ts | 1 + .../user/occ-customer-coupon.adapter.spec.ts | 38 ++++ .../user/occ-customer-coupon.adapter.ts | 15 ++ .../src/occ/occ-models/occ-endpoints.model.ts | 6 + .../customer-coupon.adapter.ts | 5 + .../customer-coupon.connector.ts | 2 +- .../coupons/my-coupons.e2e-spec-flaky.cy.ts | 11 ++ .../cypress/helpers/coupons/my-coupons.ts | 76 +++++++ .../claim-dialog/claim-dialog.component.html | 102 ++++++++++ .../claim-dialog.component.spec.ts | 186 ++++++++++++++++++ .../claim-dialog/claim-dialog.component.ts | 128 ++++++++++++ .../default-coupon-card-layout.config.ts | 6 + .../myaccount/my-coupons/index.ts | 1 + .../my-coupons/my-coupons.component.spec.ts | 24 ++- .../my-coupons/my-coupons.component.ts | 25 ++- .../myaccount/my-coupons/my-coupons.module.ts | 12 +- .../launch-dialog/config/launch-config.ts | 1 + .../scss/components/myaccount/_index.scss | 3 +- .../myaccount/_my-claim-dialog.scss | 66 +++++++ 20 files changed, 710 insertions(+), 5 deletions(-) create mode 100644 projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html create mode 100644 projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts create mode 100644 projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts create mode 100644 projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss diff --git a/projects/assets/src/translations/en/myAccount.json b/projects/assets/src/translations/en/myAccount.json index 7e55fccbbae..27b1b38b1ab 100644 --- a/projects/assets/src/translations/en/myAccount.json +++ b/projects/assets/src/translations/en/myAccount.json @@ -63,6 +63,13 @@ "findProducts": "Find Products", "status": "Status:", "dialogTitle": "Coupon", + "claimCoupondialogTitle": "Add To Your Coupon List", + "claimCouponCode": { + "label": "Coupon Code", + "placeholder": "Enter the coupon code to claim a coupon" + }, + "reset":"RESET", + "claim":"CLAIM", "claimCustomerCoupon": "You have successfully claimed this coupon.", "myCoupons": "My coupons", "startDateAsc": "Start Date (ascending)", diff --git a/projects/core/src/occ/adapters/user/default-occ-user-config.ts b/projects/core/src/occ/adapters/user/default-occ-user-config.ts index e2858c0c820..a680ed16065 100644 --- a/projects/core/src/occ/adapters/user/default-occ-user-config.ts +++ b/projects/core/src/occ/adapters/user/default-occ-user-config.ts @@ -22,6 +22,7 @@ export const defaultOccUserConfig: OccConfig = { addressVerification: 'users/${userId}/addresses/verification', customerCoupons: 'users/${userId}/customercoupons', claimCoupon: 'users/${userId}/customercoupons/${couponCode}/claim', + claimCustomerCoupon: 'users/${userId}/customercoupons/claim', couponNotification: 'users/${userId}/customercoupons/${couponCode}/notification', notificationPreference: 'users/${userId}/notificationpreferences', diff --git a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts index 52cffcd589d..8403c507129 100644 --- a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts +++ b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts @@ -235,4 +235,42 @@ describe('OccCustomerCouponAdapter', () => { mockReq.flush(customerCoupon2Customer); }); }); + describe('claim customer coupon with code in body', () => { + it('should claim a customer coupon with code in request body for a given user id', () => { + const customerCoupon: CustomerCoupon = { + couponId: couponCode, + name: 'coupon 1', + startDate: '', + endDate: '', + status: 'Effective', + description: '', + notificationOn: true, + }; + const customerCoupon2Customer: CustomerCoupon2Customer = { + coupon: customerCoupon, + customer: {}, + }; + + occCustomerCouponAdapter + .claimCustomerCouponWithCodeInBody(userId, couponCode) + .subscribe((result) => { + expect(result).toEqual(customerCoupon2Customer); + }); + + const mockReq = httpMock.expectOne((req) => { + return req.method === 'POST'; + }); + + expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('claimCustomerCoupon', { + urlParams: { + userId: userId, + }, + }); + + expect(mockReq.cancelled).toBeFalsy(); + expect(mockReq.request.body).toEqual({ couponCode: couponCode }); + expect(mockReq.request.responseType).toEqual('json'); + mockReq.flush(customerCoupon2Customer); + }); + }); }); diff --git a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts index 62b85dad52c..f9fd81ab3a4 100644 --- a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts +++ b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts @@ -78,6 +78,21 @@ export class OccCustomerCouponAdapter implements CustomerCouponAdapter { return this.http.post(url, { headers }); } + claimCustomerCouponWithCodeInBody( + userId: string, + codeVal: string + ): Observable { + + const url = this.occEndpoints.buildUrl('claimCustomerCoupon', { + urlParams: { userId}, + }); + const toClaim = { + couponCode: codeVal + }; + const headers = this.newHttpHeader(); + + return this.http.post(url, toClaim, { headers }); + } claimCustomerCoupon( userId: string, diff --git a/projects/core/src/occ/occ-models/occ-endpoints.model.ts b/projects/core/src/occ/occ-models/occ-endpoints.model.ts index 86ac698b1f6..42a2a0060b5 100644 --- a/projects/core/src/occ/occ-models/occ-endpoints.model.ts +++ b/projects/core/src/occ/occ-models/occ-endpoints.model.ts @@ -237,6 +237,12 @@ export interface OccEndpoints { * @member {string} */ claimCoupon?: string | OccEndpoint; + /** + * Endpoint for claiming coupon with code in request body + * + * @member {string} + */ + claimCustomerCoupon?: string | OccEndpoint; /** * Endpoint for coupons * diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.adapter.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.adapter.ts index 81e029587a5..41f18f79178 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.adapter.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.adapter.ts @@ -29,6 +29,11 @@ export abstract class CustomerCouponAdapter { couponCode: string ): Observable<{}>; + abstract claimCustomerCouponWithCodeInBody( + userId: string, + couponVal: string + ): Observable; + abstract claimCustomerCoupon( userId: string, couponCode: string diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts index d2b39592974..3db9a168ae0 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts @@ -43,7 +43,7 @@ export class CustomerCouponConnector { userId: string, couponCode: string ): Observable { - return this.adapter.claimCustomerCoupon(userId, couponCode); + return this.adapter.claimCustomerCouponWithCodeInBody(userId, couponCode); } disclaimCustomerCoupon( diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts index beba7761dba..1f93112bde0 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts @@ -34,6 +34,17 @@ viewportContext(['mobile', 'desktop'], () => { }); }); + describe('My coupons - claim coupons with code in body using authenticated user', () => { + beforeEach(() => { + cy.window().then((win) => { + win.sessionStorage.clear(); + }); + cy.requireLoggedIn(); + }); + myCoupons.testClaimCustomerCouponWithCodeInBody(); + + }); + describe('My coupons - Authenticated user', () => { beforeEach(() => { cy.window().then((win) => { diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts index 34ae3e040b5..77dabc17640 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts @@ -70,6 +70,14 @@ export function verifyClaimCouponSuccess(couponCode: string) { }); } +export function verifClaimCouponSuccessWithCodeInBody(couponCode: string) { + claimCouponWithCodeInBody(couponCode); + cy.location('pathname').should('contain', myCouponsUrl); + cy.get('.cx-coupon-card').within(() => { + cy.get('.cx-coupon-card-id').should('contain', couponCode); + }); +} + export function verifyClaimCouponFail(couponCode: string) { claimCoupon(couponCode); cy.location('pathname').should('contain', myCouponsUrl); @@ -78,6 +86,14 @@ export function verifyClaimCouponFail(couponCode: string) { }); } +export function verifyClaimCouponFailWithCodeInBody(couponCode: string) { + claimCouponWithCodeInBody(couponCode); + cy.location('pathname').should('contain', myCouponsUrl); + cy.get('.cx-coupon-card').within(() => { + cy.get('.cx-coupon-card-id').should('not.contain', couponCode); + }); +} + export function goMyCoupon() { cy.visit('/my-account/coupons'); cy.get('.cx-coupon-card').should('have.length', 3); @@ -115,6 +131,24 @@ export function claimCoupon(couponCode: string) { cy.wait(`@${getCoupons}`); } +export function claimCouponWithCodeInBody(couponCode: string) { + const claimCoupon = waitForClaimCouponWithCodeInBody(couponCode); + const getCoupons = waitForGetCoupons(); + const couponsPage = waitForPage(myCouponsUrl, 'getCouponsPage'); + cy.visit(myCouponsUrl + '#'+couponCode); + + verifyClaimDialog(); + cy.wait(`@${claimCoupon}`); + + cy.wait(`@${couponsPage}`); + cy.wait(`@${getCoupons}`); +} + +export function verifyResetClaimCouponCode(couponCode: string) { + cy.visit(myCouponsUrl + '#'+ couponCode); + verifyResetByClickButton(couponCode); +} + export function createStandardUser() { standardUser.registrationData.email = generateMail(randomString(), true); cy.requireLoggedIn(standardUser); @@ -174,6 +208,20 @@ export function verifyReadMore() { cy.get('.cx-dialog-header span').click(); } +export function verifyClaimDialog() { + cy.get('cx-claim-dialog').should('exist'); + cy.get('.cx-dialog-body .cx-dialog-row-submit-button .btn:first').click({ force: true }); +} + +export function verifyResetByClickButton(couponCode: string) { + cy.get('cx-claim-dialog').should('exist'); + cy.get('.cx-dialog-body input').should('have.value', couponCode); + cy.get('[formcontrolname="couponCode"]').clear().type('resetTest'); + cy.get('.cx-dialog-body input').should('have.value', 'resetTest'); + cy.get('.cx-dialog-body .cx-dialog-row--reset-button .btn:first').click({ force: true }); + cy.get('.cx-dialog-body input').should('have.value', couponCode); +} + export function verifyFindProduct(couponCode: string, productNumber: number) { const productSearchPage = waitForPage('search', 'getProductSearchPage'); @@ -214,6 +262,15 @@ export function waitForGetCoupons(): string { return `${aliasName}`; } +export function waitForClaimCouponWithCodeInBody(couponCode: string): string { + const aliasName = `claimCouponInBody_${couponCode}`; + cy.intercept({ + method: 'POST', + url: `${pageUrl}/users/current/customercoupons/claim*`, + }).as(aliasName); + return `${aliasName}`; +} + export function testClaimCustomerCoupon() { describe('Claim customer coupon', () => { it('should claim customer coupon successfully', () => { @@ -227,3 +284,22 @@ export function testClaimCustomerCoupon() { }); }); } + +export function testClaimCustomerCouponWithCodeInBody() { + describe('Claim customer coupon with code in requestBody', () => { + it('should claim customer coupon successfully with code in requestBody', () => { + verifClaimCouponSuccessWithCodeInBody(validCouponCode); + cy.saveLocalStorage(); + }); + + it('should not claim invalid customer coupon', () => { + cy.restoreLocalStorage(); + verifyClaimCouponFailWithCodeInBody(invalidCouponCode); + }); + + it('should reset coupon code val after clicking reset button', () => { + cy.restoreLocalStorage(); + verifyResetClaimCouponCode(validCouponCode); + }); + }); +} \ No newline at end of file diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html new file mode 100644 index 00000000000..dd4131955ce --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html @@ -0,0 +1,102 @@ + + + + + + \ No newline at end of file diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts new file mode 100644 index 00000000000..21908f85e82 --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts @@ -0,0 +1,186 @@ +import { Component, DebugElement, Input } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { I18nTestingModule } from '@spartacus/core'; +import { + RoutingService, + CustomerCouponService, + GlobalMessageService, + GlobalMessageType +} from '@spartacus/core'; +import { + ReactiveFormsModule, + FormControl, + FormGroup, + Validators +} from '@angular/forms'; +import { FocusDirective, FormErrorsModule } from '@spartacus/storefront'; +import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; +import { Observable, of } from 'rxjs'; +import { ICON_TYPE } from '../../../../cms-components/misc/icon/index'; +import { LaunchDialogService } from '../../../../layout/index'; +import { ClaimDialogComponent } from './claim-dialog.component'; + +const mockCoupon: string = 'testCode'; +const form = new FormGroup( +{ + couponCode: new FormControl('', [Validators.required]), +}); + +@Component({ + selector: 'cx-icon', + template: '', +}) +class MockCxIconComponent { + @Input() type: ICON_TYPE; +} +class MockLaunchDialogService implements Partial { + get data$(): Observable { + return of({coupon: 'testCode', pageSize: 10}); + } + + closeDialog(_reason: string): void {} +} + + +describe('ClaimDialogComponent', () => { + let component: ClaimDialogComponent; + let fixture: ComponentFixture; + let el: DebugElement; + let launchDialogService: LaunchDialogService; + + + const couponService = jasmine.createSpyObj('CustomerCouponService', [ + 'claimCustomerCoupon', + 'getClaimCustomerCouponResultSuccess', + 'loadCustomerCoupons' + ]); + const routingService = jasmine.createSpyObj('RoutingService', [ + 'go', + ]); + const globalMessageService = jasmine.createSpyObj('GlobalMessageService', [ + 'add', + ]); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + ClaimDialogComponent, + MockCxIconComponent, + FocusDirective, + MockFeatureDirective, + ], + imports: [ReactiveFormsModule, + I18nTestingModule,FormErrorsModule], + providers: [ + { provide: LaunchDialogService, useClass: MockLaunchDialogService }, + { provide: CustomerCouponService, useValue: couponService }, + { provide: RoutingService, useValue: routingService }, + { provide: GlobalMessageService, useValue: globalMessageService }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClaimDialogComponent); + el = fixture.debugElement; + component = fixture.componentInstance; + launchDialogService = TestBed.inject(LaunchDialogService); + component.couponCode = mockCoupon; + component.form = form; + + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should be able to show claim customer coupon dialog', () => { + fixture.detectChanges(); + const dialogTitle = fixture.debugElement.query(By.css('.cx-dialog-title')) + .nativeElement.textContent; + expect(dialogTitle).toContain('myCoupons.claimCoupondialogTitle'); + + const closeBtn = fixture.debugElement.query(By.css('button')); + expect(closeBtn).toBeTruthy(); + + + const couponLabel = fixture.debugElement.query( + By.css('.cx-dialog-body .label-content') + ).nativeElement.textContent; + expect(couponLabel).toContain('myCoupons.claimCouponCode.label'); + + }); + + it('should be able to close dialog', () => { + spyOn(launchDialogService, 'closeDialog').and.stub(); + fixture.detectChanges(); + const closeBtn = fixture.debugElement.query(By.css('button')); + closeBtn.nativeElement.click(); + expect(launchDialogService.closeDialog).toHaveBeenCalled(); + }); + + it('should emit handleClick event', () => { + spyOn(component, 'handleClick').and.callThrough(); + spyOn(component, 'close'); + + expect(component.handleClick).toHaveBeenCalledTimes(0); + + el.nativeElement.click(); + fixture.detectChanges(); + + expect(component.handleClick).toHaveBeenCalledTimes(1); + expect(component.close).toHaveBeenCalledWith('Cross click'); + }); + + describe('Form Interactions', () => { + + it('should reset the coupon code after click reset button', () => { + + component.ngOnInit(); + expect(component.couponCode).toBe(mockCoupon); + + (form.get('couponCode') as FormControl).setValue('testcodechanged'); + + component.cancelEdit(); + fixture.detectChanges(); + expect((form.get('couponCode') as FormControl).value).toBe(mockCoupon); + }); + + it('should succeed on submit', () => { + (form.get('couponCode') as FormControl).setValue(mockCoupon); + fixture.detectChanges(); + couponService.claimCustomerCoupon.and.stub(); + couponService.loadCustomerCoupons.and.stub(); + couponService.getClaimCustomerCouponResultSuccess.and.returnValue(of(true)); + routingService.go.and.stub(); + globalMessageService.add.and.stub(); + component.onSubmit(); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'myCoupons.claimCustomerCoupon' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'coupons' }); + expect(couponService.claimCustomerCoupon).toHaveBeenCalledTimes(1); + expect(couponService.loadCustomerCoupons).toHaveBeenCalledTimes(1); + + }); + + it('should fail on submit', () => { + (form.get('couponCode') as FormControl).setValue(mockCoupon); + fixture.detectChanges(); + couponService.claimCustomerCoupon.and.stub(); + couponService.loadCustomerCoupons.and.stub(); + couponService.getClaimCustomerCouponResultSuccess.and.returnValue(of(false)); + routingService.go.and.stub(); + globalMessageService.add.and.stub(); + component.onSubmit(); + expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'coupons' }); + + }); + + }); + +}); diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts new file mode 100644 index 00000000000..6d335d88c6d --- /dev/null +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + ElementRef, + HostListener, +} from '@angular/core'; +import { + FormControl, + FormGroup, + Validators, + } from '@angular/forms'; + +import { Subscription } from 'rxjs'; +import { + RoutingService, + CustomerCouponService, + GlobalMessageService, + GlobalMessageType, + useFeatureStyles, +} from '@spartacus/core'; +import { FocusConfig, LaunchDialogService } from '../../../../layout/index'; +import { ICON_TYPE } from '../../../../cms-components/misc/icon/index'; + +@Component({ + selector: 'cx-claim-dialog', + templateUrl: './claim-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + +}) + +export class ClaimDialogComponent implements OnDestroy, OnInit { + + private subscription = new Subscription(); + iconTypes = ICON_TYPE; + private pageSize = 10; + + couponCode: string; + + focusConfig: FocusConfig = { + trap: true, + block: true, + autofocus: 'button', + focusOnEscape: true, + }; + + form: FormGroup = new FormGroup( + { + couponCode: new FormControl('', [Validators.required]), + } + ); + + @HostListener('click', ['$event']) + handleClick(event: UIEvent): void { + if ((event.target as any).tagName === this.el.nativeElement.tagName) { + this.close('Cross click'); + } + } + constructor( + protected couponService: CustomerCouponService, + protected routingService: RoutingService, + protected messageService: GlobalMessageService, + protected launchDialogService: LaunchDialogService, + protected el: ElementRef + + ) { + useFeatureStyles('a11yExpandedFocusIndicator'); + } + + ngOnInit(): void { + this.subscription.add( + this.launchDialogService.data$.subscribe((data) => { + if (data) { + this.couponCode = data.coupon; + this.pageSize=data.pageSize; + (this.form.get('couponCode') as FormControl).setValue(this.couponCode); + } + }) + ); + } + + onSubmit(): void { + if (!this.form.valid) { + this.form.markAllAsTouched(); + return; + } + const couponVal = (this.form.get('couponCode') as FormControl).value; + if (couponVal) { + this.couponService.claimCustomerCoupon(couponVal); + this.subscription = this.couponService + .getClaimCustomerCouponResultSuccess() + .subscribe((success) => { + if (success) { + this.messageService.add( + { key: 'myCoupons.claimCustomerCoupon' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + } + this.routingService.go({ cxRoute: 'coupons'}); + this.couponService.loadCustomerCoupons(this.pageSize); + this.close('Cross click'); + }); + } else { + this.routingService.go({ cxRoute: 'notFound' }); + } + + } + + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + + close(reason?: any): void { + this.launchDialogService.closeDialog(reason); + } + + cancelEdit(): void { + (this.form.get('couponCode') as FormControl).setValue(this.couponCode); + } +} diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts index a2efb49e111..0d099c61c4a 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts @@ -6,6 +6,7 @@ import { DIALOG_TYPE, LayoutConfig } from '../../../layout/index'; import { CouponDialogComponent } from './coupon-card/coupon-dialog/coupon-dialog.component'; +import { ClaimDialogComponent } from './claim-dialog/claim-dialog.component' export const defaultCouponLayoutConfig: LayoutConfig = { launch: { @@ -14,5 +15,10 @@ export const defaultCouponLayoutConfig: LayoutConfig = { component: CouponDialogComponent, dialogType: DIALOG_TYPE.DIALOG, }, + CLAIM_DIALOG: { + inlineRoot: true, + component: ClaimDialogComponent, + dialogType: DIALOG_TYPE.DIALOG, + }, }, }; diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/index.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/index.ts index 72d79ea90ce..446efc231c6 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/index.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/index.ts @@ -8,5 +8,6 @@ export * from './my-coupons.component'; export * from './my-coupons.module'; export * from './coupon-card/coupon-card.component'; export * from './coupon-card/coupon-dialog/coupon-dialog.component'; +export * from './claim-dialog/claim-dialog.component'; export * from './coupon-claim/coupon-claim.component'; export * from './my-coupons.component.service'; diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts index 2669ecf45f7..14b926b0c1f 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts @@ -2,6 +2,7 @@ import { Component, DebugElement, EventEmitter, + ElementRef, Input, Output, } from '@angular/core'; @@ -15,8 +16,9 @@ import { FeaturesConfig, I18nTestingModule, } from '@spartacus/core'; +import { LAUNCH_CALLER, LaunchDialogService } from '../../../layout/index'; import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; import { SpinnerModule } from '../../../shared/components/spinner/spinner.module'; import { ICON_TYPE } from '../../misc/icon/icon.model'; import { MyCouponsComponent } from './my-coupons.component'; @@ -143,10 +145,20 @@ class MockSortingComponent { @Output() sortListEvent = new EventEmitter(); } +class MockLaunchDialogService implements Partial { + openDialogAndSubscribe( + _caller: LAUNCH_CALLER, + _openElement?: ElementRef, + ) { + return EMPTY; + } +} + describe('MyCouponsComponent', () => { let component: MyCouponsComponent; let fixture: ComponentFixture; let el: DebugElement; + let launchDialogService: LaunchDialogService; const customerCouponService = jasmine.createSpyObj('CustomerCouponService', [ 'getCustomerCoupons', @@ -183,6 +195,7 @@ describe('MyCouponsComponent', () => { provide: MyCouponsComponentService, useValue: myCouponsComponentService, }, + { provide: LaunchDialogService, useClass: MockLaunchDialogService }, { provide: FeaturesConfig, useValue: { @@ -197,6 +210,7 @@ describe('MyCouponsComponent', () => { fixture = TestBed.createComponent(MyCouponsComponent); component = fixture.componentInstance; el = fixture.debugElement; + launchDialogService = TestBed.inject(LaunchDialogService); customerCouponService.getCustomerCoupons.and.returnValue( of(emptyCouponResult) @@ -318,4 +332,12 @@ describe('MyCouponsComponent', () => { PAGE_SIZE ); }); + + it('should be able to open coupon claim dialog if has hash str in location', () => { + spyOn(component,"getHashStr").and.returnValue(String('#testcode')); + component.ngOnInit(); + spyOn(launchDialogService, 'openDialogAndSubscribe').and.stub(); + fixture.detectChanges(); + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + }); }); diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts index 5f864926fe5..22cb2fec623 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts @@ -13,6 +13,7 @@ import { import { combineLatest, Observable, Subscription } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { ICON_TYPE } from '../../misc/icon/icon.model'; +import { LaunchDialogService, LAUNCH_CALLER } from '../../../layout/index'; import { MyCouponsComponentService } from './my-coupons.component.service'; @Component({ @@ -66,7 +67,8 @@ export class MyCouponsComponent implements OnInit, OnDestroy { constructor( protected couponService: CustomerCouponService, - protected myCouponsComponentService: MyCouponsComponentService + protected myCouponsComponentService: MyCouponsComponentService, + protected launchDialogService: LaunchDialogService ) {} ngOnInit(): void { @@ -107,6 +109,27 @@ export class MyCouponsComponent implements OnInit, OnDestroy { this.subscriptionFail(error); }) ); + + var resultStr = decodeURIComponent(this.getHashStr()); + var index = resultStr.indexOf('#'); + if(index !== -1) + { + const couponCode=resultStr.substring(index + 1); + if(couponCode!==undefined && couponCode.length>0) + { + this.launchDialogService.openDialogAndSubscribe( + LAUNCH_CALLER.CLAIM_DIALOG, + undefined, + { coupon: couponCode, pageSize: this.PAGE_SIZE } + ); + + } + } + } + + getHashStr() + { + return location.hash; } private subscriptionFail(error: boolean) { diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts index 3bc89d2d92b..4be8cdfcc6a 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts @@ -7,6 +7,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { ReactiveFormsModule} from '@angular/forms'; import { AuthGuard, CmsConfig, @@ -21,16 +22,20 @@ import { KeyboardFocusModule } from '../../../layout/index'; import { CardModule } from '../../../shared/components/card/card.module'; import { ListNavigationModule } from '../../../shared/components/list-navigation/list-navigation.module'; import { SpinnerModule } from '../../../shared/components/spinner/spinner.module'; +import { FormErrorsModule } from '../../../shared/components/form/form-errors'; import { IconModule } from '../../misc/icon/icon.module'; import { CouponCardComponent } from './coupon-card/coupon-card.component'; import { CouponDialogComponent } from './coupon-card/coupon-dialog/coupon-dialog.component'; import { CouponClaimComponent } from './coupon-claim/coupon-claim.component'; +import { ClaimDialogComponent } from './claim-dialog/claim-dialog.component'; import { defaultCouponLayoutConfig } from './default-coupon-card-layout.config'; import { MyCouponsComponent } from './my-coupons.component'; @NgModule({ imports: [ CommonModule, + ReactiveFormsModule, + FormErrorsModule, CardModule, SpinnerModule, I18nModule, @@ -55,6 +60,7 @@ import { MyCouponsComponent } from './my-coupons.component'; CouponCardComponent, CouponDialogComponent, CouponClaimComponent, + ClaimDialogComponent, ], providers: [ provideDefaultConfig({ @@ -67,10 +73,14 @@ import { MyCouponsComponent } from './my-coupons.component'; component: CouponClaimComponent, guards: [AuthGuard], }, + ClaimDialogComponent: { + component: ClaimDialogComponent, + guards: [AuthGuard], + }, }, }), provideDefaultConfig(defaultCouponLayoutConfig), ], - exports: [MyCouponsComponent, CouponClaimComponent], + exports: [MyCouponsComponent, CouponClaimComponent, ClaimDialogComponent], }) export class MyCouponsModule {} diff --git a/projects/storefrontlib/layout/launch-dialog/config/launch-config.ts b/projects/storefrontlib/layout/launch-dialog/config/launch-config.ts index 8a086e1886e..a90de59d2c0 100644 --- a/projects/storefrontlib/layout/launch-dialog/config/launch-config.ts +++ b/projects/storefrontlib/layout/launch-dialog/config/launch-config.ts @@ -98,5 +98,6 @@ export const enum LAUNCH_CALLER { PLACE_ORDER_SPINNER = 'PLACE_ORDER_SPINNER', SUGGESTED_ADDRESSES = 'SUGGESTED_ADDRESSES', COUPON = 'COUPON', + CLAIM_DIALOG = 'CLAIM_DIALOG', STOCK_NOTIFICATION = 'STOCK_NOTIFICATION', } diff --git a/projects/storefrontstyles/scss/components/myaccount/_index.scss b/projects/storefrontstyles/scss/components/myaccount/_index.scss index ba7a3d4de36..685c95c237c 100644 --- a/projects/storefrontstyles/scss/components/myaccount/_index.scss +++ b/projects/storefrontstyles/scss/components/myaccount/_index.scss @@ -9,12 +9,13 @@ @import './my-coupons'; @import './my-coupons-card'; @import './my-coupons-dialog'; +@import './my-claim-dialog'; @import './my-interests'; @import './cx-my-account-v2-notification-preference'; $myaccount-components-allowlist: cx-anonymous-consent-management-banner, cx-anonymous-consent-dialog, cx-anonymous-consent-open-dialog, cx-consent-management-form, cx-consent-management, cx-my-interests, - cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-payment-methods, + cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-claim-dialog, cx-payment-methods, cx-my-account-v2-notification-preference, cx-my-account-v2-consent-management-form, cx-my-account-v2-consent-management !default; diff --git a/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss b/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss new file mode 100644 index 00000000000..7c25cf62726 --- /dev/null +++ b/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss @@ -0,0 +1,66 @@ +%cx-claim-dialog { + background-color: rgba(0, 0, 0, 0.5); + .cx-coupon-dialog { + @extend .modal-dialog; + @extend .modal-dialog-centered; + @extend .modal-lg; + + .cx-coupon-container { + @extend .modal-content; + .cx-dialog-item { + padding-inline-end: 1.75rem; + padding-inline-start: 1.75rem; + } + + .cx-dialog-header { + padding-top: 2rem; + padding-inline-end: 1.75rem; + padding-bottom: 0.85rem; + padding-inline-start: 5.75rem; + border-width: 0; + @include cx-highContrastTheme { + background-color: var(--cx-color-background); + } + } + + .cx-dialog-title { + @include type('3'); + } + + .cx-dialog-body { + padding-top: 1rem; + padding-inline-end: 5.75rem; + padding-bottom: 0; + padding-inline-start: 5.75rem; + @include media-breakpoint-down(sm) { + padding: 0; + } + @include cx-highContrastTheme { + background-color: var(--cx-color-background); + } + } + + .cx-dialog-row { + margin: 0; + display: flex; + padding: 0 0 2.875rem; + max-width: 100%; + margin-top: 2.875rem; + margin-bottom: 1.5rem; + + @include media-breakpoint-down(sm) { + padding: 0; + } + } + + .cx-dialog-row--reset-button { + padding:0 12px 0 0 + } + + .cx-dialog-row-submit-button { + padding:0 0 0 12px + } + + } + } +} From d4587da43b41055b575b2ad173544c8c82948403 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Mon, 9 Dec 2024 19:52:32 +0800 Subject: [PATCH 02/15] fix failed unit test --- .../customer-coupon.connector.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts index ff670877e56..d07f300693e 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts @@ -21,6 +21,9 @@ class MockUserAdapter implements CustomerCouponAdapter { claimCustomerCoupon = createSpy('claimCustomerCoupon').and.callFake( (userId) => of(`claim-${userId}`) ); + claimCustomerCouponWithCodeInBody = createSpy('claimCustomerCouponWithCodeInBody').and.callFake( + (userId) => of(`claim-${userId}`) + ); disclaimCustomerCoupon = createSpy('disclaimCustomerCoupon').and.callFake( (userId) => of(`disclaim-${userId}`) ); @@ -95,6 +98,18 @@ describe('CustomerCouponConnector', () => { ); }); + it('claimCustomerCouponWithCodeInBody should call adapter', () => { + let result; + service + .claimCustomerCoupon('userId', 'couponCode') + .subscribe((res) => (result = res)); + expect(result).toEqual('claim-userId'); + expect(adapter.claimCustomerCouponWithCodeInBody).toHaveBeenCalledWith( + 'userId', + 'couponCode' + ); + }); + it('disclaimCustomerCoupon should call adapter', () => { let result; service From 9ba5cd1246b180d9716d11a4a5852d850ee4f540 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Mon, 9 Dec 2024 20:20:32 +0800 Subject: [PATCH 03/15] fix failed unit test cases --- .../customer-coupon.connector.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts index d07f300693e..e8df5bb8943 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts @@ -87,18 +87,6 @@ describe('CustomerCouponConnector', () => { }); it('claimCustomerCoupon should call adapter', () => { - let result; - service - .claimCustomerCoupon('userId', 'couponCode') - .subscribe((res) => (result = res)); - expect(result).toEqual('claim-userId'); - expect(adapter.claimCustomerCoupon).toHaveBeenCalledWith( - 'userId', - 'couponCode' - ); - }); - - it('claimCustomerCouponWithCodeInBody should call adapter', () => { let result; service .claimCustomerCoupon('userId', 'couponCode') From 5388aa2556a8335c40c106bdb16df8f490ea90a3 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Tue, 10 Dec 2024 10:10:26 +0800 Subject: [PATCH 04/15] fix sonar issues --- .../myaccount/my-coupons/my-coupons.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts index 22cb2fec623..77fc743926d 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts @@ -110,8 +110,8 @@ export class MyCouponsComponent implements OnInit, OnDestroy { }) ); - var resultStr = decodeURIComponent(this.getHashStr()); - var index = resultStr.indexOf('#'); + let resultStr = decodeURIComponent(this.getHashStr()); + const index = resultStr.indexOf('#'); if(index !== -1) { const couponCode=resultStr.substring(index + 1); From 967974e72bbc6ccb7b783c3f5eed7080af56201d Mon Sep 17 00:00:00 2001 From: Cris Li Date: Tue, 10 Dec 2024 10:21:11 +0800 Subject: [PATCH 05/15] fix sonar issues using const instead of let --- .../cms-components/myaccount/my-coupons/my-coupons.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts index 77fc743926d..691c73b38e1 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts @@ -110,7 +110,7 @@ export class MyCouponsComponent implements OnInit, OnDestroy { }) ); - let resultStr = decodeURIComponent(this.getHashStr()); + const resultStr = decodeURIComponent(this.getHashStr()); const index = resultStr.indexOf('#'); if(index !== -1) { From bce0e55aa4dde394a3f5011a0709d78b079389f6 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Tue, 10 Dec 2024 10:38:05 +0800 Subject: [PATCH 06/15] fix code validation failed issues --- projects/core/src/occ/occ-models/occ-endpoints.model.ts | 8 ++++---- .../my-coupons/default-coupon-card-layout.config.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/core/src/occ/occ-models/occ-endpoints.model.ts b/projects/core/src/occ/occ-models/occ-endpoints.model.ts index 42a2a0060b5..56e85bc4bee 100644 --- a/projects/core/src/occ/occ-models/occ-endpoints.model.ts +++ b/projects/core/src/occ/occ-models/occ-endpoints.model.ts @@ -238,10 +238,10 @@ export interface OccEndpoints { */ claimCoupon?: string | OccEndpoint; /** - * Endpoint for claiming coupon with code in request body - * - * @member {string} - */ + * Endpoint for claiming coupon with code in request body + * + * @member {string} + */ claimCustomerCoupon?: string | OccEndpoint; /** * Endpoint for coupons diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts index 0d099c61c4a..4f52718cd84 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/default-coupon-card-layout.config.ts @@ -6,7 +6,7 @@ import { DIALOG_TYPE, LayoutConfig } from '../../../layout/index'; import { CouponDialogComponent } from './coupon-card/coupon-dialog/coupon-dialog.component'; -import { ClaimDialogComponent } from './claim-dialog/claim-dialog.component' +import { ClaimDialogComponent } from './claim-dialog/claim-dialog.component'; export const defaultCouponLayoutConfig: LayoutConfig = { launch: { From 31a383f8091345851966f6304a3772ed18f6d523 Mon Sep 17 00:00:00 2001 From: Cris Li Date: Tue, 10 Dec 2024 11:04:15 +0800 Subject: [PATCH 07/15] do code format for the PR --- .../assets/src/translations/en/myAccount.json | 4 +- .../user/occ-customer-coupon.adapter.spec.ts | 13 ++-- .../user/occ-customer-coupon.adapter.ts | 5 +- .../src/occ/occ-models/occ-endpoints.model.ts | 12 ++-- .../customer-coupon.connector.spec.ts | 6 +- .../coupons/my-coupons.e2e-spec-flaky.cy.ts | 3 +- .../cypress/helpers/coupons/my-coupons.ts | 14 ++-- .../claim-dialog/claim-dialog.component.html | 46 ++++++------- .../claim-dialog.component.spec.ts | 41 +++++------- .../claim-dialog/claim-dialog.component.ts | 64 ++++++++----------- .../my-coupons/my-coupons.component.spec.ts | 9 +-- .../my-coupons/my-coupons.component.ts | 22 +++---- .../myaccount/my-coupons/my-coupons.module.ts | 2 +- .../scss/components/myaccount/_index.scss | 4 +- .../myaccount/_my-claim-dialog.scss | 5 +- 15 files changed, 113 insertions(+), 137 deletions(-) diff --git a/projects/assets/src/translations/en/myAccount.json b/projects/assets/src/translations/en/myAccount.json index 27b1b38b1ab..f370006eed1 100644 --- a/projects/assets/src/translations/en/myAccount.json +++ b/projects/assets/src/translations/en/myAccount.json @@ -68,8 +68,8 @@ "label": "Coupon Code", "placeholder": "Enter the coupon code to claim a coupon" }, - "reset":"RESET", - "claim":"CLAIM", + "reset": "RESET", + "claim": "CLAIM", "claimCustomerCoupon": "You have successfully claimed this coupon.", "myCoupons": "My coupons", "startDateAsc": "Start Date (ascending)", diff --git a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts index 8403c507129..de0216173ba 100644 --- a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts +++ b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.spec.ts @@ -261,11 +261,14 @@ describe('OccCustomerCouponAdapter', () => { return req.method === 'POST'; }); - expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('claimCustomerCoupon', { - urlParams: { - userId: userId, - }, - }); + expect(occEnpointsService.buildUrl).toHaveBeenCalledWith( + 'claimCustomerCoupon', + { + urlParams: { + userId: userId, + }, + } + ); expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.body).toEqual({ couponCode: couponCode }); diff --git a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts index f9fd81ab3a4..36e7057d3ad 100644 --- a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts +++ b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts @@ -82,12 +82,11 @@ export class OccCustomerCouponAdapter implements CustomerCouponAdapter { userId: string, codeVal: string ): Observable { - const url = this.occEndpoints.buildUrl('claimCustomerCoupon', { - urlParams: { userId}, + urlParams: { userId }, }); const toClaim = { - couponCode: codeVal + couponCode: codeVal, }; const headers = this.newHttpHeader(); diff --git a/projects/core/src/occ/occ-models/occ-endpoints.model.ts b/projects/core/src/occ/occ-models/occ-endpoints.model.ts index 56e85bc4bee..61a90863bcd 100644 --- a/projects/core/src/occ/occ-models/occ-endpoints.model.ts +++ b/projects/core/src/occ/occ-models/occ-endpoints.model.ts @@ -237,12 +237,12 @@ export interface OccEndpoints { * @member {string} */ claimCoupon?: string | OccEndpoint; - /** - * Endpoint for claiming coupon with code in request body - * - * @member {string} - */ - claimCustomerCoupon?: string | OccEndpoint; + /** + * Endpoint for claiming coupon with code in request body + * + * @member {string} + */ + claimCustomerCoupon?: string | OccEndpoint; /** * Endpoint for coupons * diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts index e8df5bb8943..d95064a99d0 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts @@ -21,9 +21,9 @@ class MockUserAdapter implements CustomerCouponAdapter { claimCustomerCoupon = createSpy('claimCustomerCoupon').and.callFake( (userId) => of(`claim-${userId}`) ); - claimCustomerCouponWithCodeInBody = createSpy('claimCustomerCouponWithCodeInBody').and.callFake( - (userId) => of(`claim-${userId}`) - ); + claimCustomerCouponWithCodeInBody = createSpy( + 'claimCustomerCouponWithCodeInBody' + ).and.callFake((userId) => of(`claim-${userId}`)); disclaimCustomerCoupon = createSpy('disclaimCustomerCoupon').and.callFake( (userId) => of(`disclaim-${userId}`) ); diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts index 1f93112bde0..62ccc630fe9 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/coupons/my-coupons.e2e-spec-flaky.cy.ts @@ -40,9 +40,8 @@ viewportContext(['mobile', 'desktop'], () => { win.sessionStorage.clear(); }); cy.requireLoggedIn(); - }); + }); myCoupons.testClaimCustomerCouponWithCodeInBody(); - }); describe('My coupons - Authenticated user', () => { diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts index 77dabc17640..56e0c34e682 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts @@ -135,7 +135,7 @@ export function claimCouponWithCodeInBody(couponCode: string) { const claimCoupon = waitForClaimCouponWithCodeInBody(couponCode); const getCoupons = waitForGetCoupons(); const couponsPage = waitForPage(myCouponsUrl, 'getCouponsPage'); - cy.visit(myCouponsUrl + '#'+couponCode); + cy.visit(myCouponsUrl + '#' + couponCode); verifyClaimDialog(); cy.wait(`@${claimCoupon}`); @@ -145,7 +145,7 @@ export function claimCouponWithCodeInBody(couponCode: string) { } export function verifyResetClaimCouponCode(couponCode: string) { - cy.visit(myCouponsUrl + '#'+ couponCode); + cy.visit(myCouponsUrl + '#' + couponCode); verifyResetByClickButton(couponCode); } @@ -210,7 +210,9 @@ export function verifyReadMore() { export function verifyClaimDialog() { cy.get('cx-claim-dialog').should('exist'); - cy.get('.cx-dialog-body .cx-dialog-row-submit-button .btn:first').click({ force: true }); + cy.get('.cx-dialog-body .cx-dialog-row-submit-button .btn:first').click({ + force: true, + }); } export function verifyResetByClickButton(couponCode: string) { @@ -218,7 +220,9 @@ export function verifyResetByClickButton(couponCode: string) { cy.get('.cx-dialog-body input').should('have.value', couponCode); cy.get('[formcontrolname="couponCode"]').clear().type('resetTest'); cy.get('.cx-dialog-body input').should('have.value', 'resetTest'); - cy.get('.cx-dialog-body .cx-dialog-row--reset-button .btn:first').click({ force: true }); + cy.get('.cx-dialog-body .cx-dialog-row--reset-button .btn:first').click({ + force: true, + }); cy.get('.cx-dialog-body input').should('have.value', couponCode); } @@ -302,4 +306,4 @@ export function testClaimCustomerCouponWithCodeInBody() { verifyResetClaimCouponCode(validCouponCode); }); }); -} \ No newline at end of file +} diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html index dd4131955ce..7fee7c2aa73 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html @@ -38,7 +38,6 @@ - * - \ No newline at end of file + diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts index 21908f85e82..90cb10d3de7 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.spec.ts @@ -1,18 +1,18 @@ import { Component, DebugElement, Input } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { I18nTestingModule } from '@spartacus/core'; +import { I18nTestingModule } from '@spartacus/core'; import { RoutingService, CustomerCouponService, GlobalMessageService, - GlobalMessageType + GlobalMessageType, } from '@spartacus/core'; import { ReactiveFormsModule, FormControl, FormGroup, - Validators + Validators, } from '@angular/forms'; import { FocusDirective, FormErrorsModule } from '@spartacus/storefront'; import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive'; @@ -22,9 +22,8 @@ import { LaunchDialogService } from '../../../../layout/index'; import { ClaimDialogComponent } from './claim-dialog.component'; const mockCoupon: string = 'testCode'; -const form = new FormGroup( -{ - couponCode: new FormControl('', [Validators.required]), +const form = new FormGroup({ + couponCode: new FormControl('', [Validators.required]), }); @Component({ @@ -36,28 +35,24 @@ class MockCxIconComponent { } class MockLaunchDialogService implements Partial { get data$(): Observable { - return of({coupon: 'testCode', pageSize: 10}); + return of({ coupon: 'testCode', pageSize: 10 }); } closeDialog(_reason: string): void {} } - describe('ClaimDialogComponent', () => { let component: ClaimDialogComponent; let fixture: ComponentFixture; let el: DebugElement; let launchDialogService: LaunchDialogService; - const couponService = jasmine.createSpyObj('CustomerCouponService', [ 'claimCustomerCoupon', 'getClaimCustomerCouponResultSuccess', - 'loadCustomerCoupons' - ]); - const routingService = jasmine.createSpyObj('RoutingService', [ - 'go', + 'loadCustomerCoupons', ]); + const routingService = jasmine.createSpyObj('RoutingService', ['go']); const globalMessageService = jasmine.createSpyObj('GlobalMessageService', [ 'add', ]); @@ -70,8 +65,7 @@ describe('ClaimDialogComponent', () => { FocusDirective, MockFeatureDirective, ], - imports: [ReactiveFormsModule, - I18nTestingModule,FormErrorsModule], + imports: [ReactiveFormsModule, I18nTestingModule, FormErrorsModule], providers: [ { provide: LaunchDialogService, useClass: MockLaunchDialogService }, { provide: CustomerCouponService, useValue: couponService }, @@ -88,7 +82,6 @@ describe('ClaimDialogComponent', () => { launchDialogService = TestBed.inject(LaunchDialogService); component.couponCode = mockCoupon; component.form = form; - }); it('should create', () => { @@ -105,12 +98,10 @@ describe('ClaimDialogComponent', () => { const closeBtn = fixture.debugElement.query(By.css('button')); expect(closeBtn).toBeTruthy(); - const couponLabel = fixture.debugElement.query( By.css('.cx-dialog-body .label-content') ).nativeElement.textContent; expect(couponLabel).toContain('myCoupons.claimCouponCode.label'); - }); it('should be able to close dialog', () => { @@ -135,9 +126,7 @@ describe('ClaimDialogComponent', () => { }); describe('Form Interactions', () => { - it('should reset the coupon code after click reset button', () => { - component.ngOnInit(); expect(component.couponCode).toBe(mockCoupon); @@ -153,7 +142,9 @@ describe('ClaimDialogComponent', () => { fixture.detectChanges(); couponService.claimCustomerCoupon.and.stub(); couponService.loadCustomerCoupons.and.stub(); - couponService.getClaimCustomerCouponResultSuccess.and.returnValue(of(true)); + couponService.getClaimCustomerCouponResultSuccess.and.returnValue( + of(true) + ); routingService.go.and.stub(); globalMessageService.add.and.stub(); component.onSubmit(); @@ -165,7 +156,6 @@ describe('ClaimDialogComponent', () => { expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'coupons' }); expect(couponService.claimCustomerCoupon).toHaveBeenCalledTimes(1); expect(couponService.loadCustomerCoupons).toHaveBeenCalledTimes(1); - }); it('should fail on submit', () => { @@ -173,14 +163,13 @@ describe('ClaimDialogComponent', () => { fixture.detectChanges(); couponService.claimCustomerCoupon.and.stub(); couponService.loadCustomerCoupons.and.stub(); - couponService.getClaimCustomerCouponResultSuccess.and.returnValue(of(false)); + couponService.getClaimCustomerCouponResultSuccess.and.returnValue( + of(false) + ); routingService.go.and.stub(); globalMessageService.add.and.stub(); component.onSubmit(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'coupons' }); - }); - }); - }); diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts index 6d335d88c6d..6434daa91d0 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.ts @@ -12,11 +12,7 @@ import { ElementRef, HostListener, } from '@angular/core'; -import { - FormControl, - FormGroup, - Validators, - } from '@angular/forms'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Subscription } from 'rxjs'; import { @@ -33,11 +29,8 @@ import { ICON_TYPE } from '../../../../cms-components/misc/icon/index'; selector: 'cx-claim-dialog', templateUrl: './claim-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - }) - export class ClaimDialogComponent implements OnDestroy, OnInit { - private subscription = new Subscription(); iconTypes = ICON_TYPE; private pageSize = 10; @@ -51,11 +44,9 @@ export class ClaimDialogComponent implements OnDestroy, OnInit { focusOnEscape: true, }; - form: FormGroup = new FormGroup( - { - couponCode: new FormControl('', [Validators.required]), - } - ); + form: FormGroup = new FormGroup({ + couponCode: new FormControl('', [Validators.required]), + }); @HostListener('click', ['$event']) handleClick(event: UIEvent): void { @@ -69,7 +60,6 @@ export class ClaimDialogComponent implements OnDestroy, OnInit { protected messageService: GlobalMessageService, protected launchDialogService: LaunchDialogService, protected el: ElementRef - ) { useFeatureStyles('a11yExpandedFocusIndicator'); } @@ -79,8 +69,10 @@ export class ClaimDialogComponent implements OnDestroy, OnInit { this.launchDialogService.data$.subscribe((data) => { if (data) { this.couponCode = data.coupon; - this.pageSize=data.pageSize; - (this.form.get('couponCode') as FormControl).setValue(this.couponCode); + this.pageSize = data.pageSize; + (this.form.get('couponCode') as FormControl).setValue( + this.couponCode + ); } }) ); @@ -91,29 +83,27 @@ export class ClaimDialogComponent implements OnDestroy, OnInit { this.form.markAllAsTouched(); return; } - const couponVal = (this.form.get('couponCode') as FormControl).value; - if (couponVal) { - this.couponService.claimCustomerCoupon(couponVal); - this.subscription = this.couponService - .getClaimCustomerCouponResultSuccess() - .subscribe((success) => { - if (success) { - this.messageService.add( - { key: 'myCoupons.claimCustomerCoupon' }, - GlobalMessageType.MSG_TYPE_CONFIRMATION - ); - } - this.routingService.go({ cxRoute: 'coupons'}); - this.couponService.loadCustomerCoupons(this.pageSize); - this.close('Cross click'); - }); - } else { - this.routingService.go({ cxRoute: 'notFound' }); - } - + const couponVal = (this.form.get('couponCode') as FormControl).value; + if (couponVal) { + this.couponService.claimCustomerCoupon(couponVal); + this.subscription = this.couponService + .getClaimCustomerCouponResultSuccess() + .subscribe((success) => { + if (success) { + this.messageService.add( + { key: 'myCoupons.claimCustomerCoupon' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + } + this.routingService.go({ cxRoute: 'coupons' }); + this.couponService.loadCustomerCoupons(this.pageSize); + this.close('Cross click'); + }); + } else { + this.routingService.go({ cxRoute: 'notFound' }); + } } - ngOnDestroy(): void { this.subscription?.unsubscribe(); } diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts index 14b926b0c1f..3cfaf810207 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.spec.ts @@ -146,10 +146,7 @@ class MockSortingComponent { } class MockLaunchDialogService implements Partial { - openDialogAndSubscribe( - _caller: LAUNCH_CALLER, - _openElement?: ElementRef, - ) { + openDialogAndSubscribe(_caller: LAUNCH_CALLER, _openElement?: ElementRef) { return EMPTY; } } @@ -334,10 +331,10 @@ describe('MyCouponsComponent', () => { }); it('should be able to open coupon claim dialog if has hash str in location', () => { - spyOn(component,"getHashStr").and.returnValue(String('#testcode')); + spyOn(component, 'getHashStr').and.returnValue(String('#testcode')); component.ngOnInit(); spyOn(launchDialogService, 'openDialogAndSubscribe').and.stub(); fixture.detectChanges(); expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); - }); + }); }); diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts index 691c73b38e1..dbfe846e278 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.component.ts @@ -112,23 +112,19 @@ export class MyCouponsComponent implements OnInit, OnDestroy { const resultStr = decodeURIComponent(this.getHashStr()); const index = resultStr.indexOf('#'); - if(index !== -1) - { - const couponCode=resultStr.substring(index + 1); - if(couponCode!==undefined && couponCode.length>0) - { - this.launchDialogService.openDialogAndSubscribe( - LAUNCH_CALLER.CLAIM_DIALOG, - undefined, - { coupon: couponCode, pageSize: this.PAGE_SIZE } - ); - + if (index !== -1) { + const couponCode = resultStr.substring(index + 1); + if (couponCode !== undefined && couponCode.length > 0) { + this.launchDialogService.openDialogAndSubscribe( + LAUNCH_CALLER.CLAIM_DIALOG, + undefined, + { coupon: couponCode, pageSize: this.PAGE_SIZE } + ); } } } - getHashStr() - { + getHashStr() { return location.hash; } diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts index 4be8cdfcc6a..2e9dc27ece4 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/my-coupons.module.ts @@ -7,7 +7,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { ReactiveFormsModule} from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { AuthGuard, CmsConfig, diff --git a/projects/storefrontstyles/scss/components/myaccount/_index.scss b/projects/storefrontstyles/scss/components/myaccount/_index.scss index 685c95c237c..e922e308cc5 100644 --- a/projects/storefrontstyles/scss/components/myaccount/_index.scss +++ b/projects/storefrontstyles/scss/components/myaccount/_index.scss @@ -16,6 +16,6 @@ $myaccount-components-allowlist: cx-anonymous-consent-management-banner, cx-anonymous-consent-dialog, cx-anonymous-consent-open-dialog, cx-consent-management-form, cx-consent-management, cx-my-interests, - cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-claim-dialog, cx-payment-methods, - cx-my-account-v2-notification-preference, + cx-my-coupons, cx-coupon-card, cx-coupon-dialog, cx-claim-dialog, + cx-payment-methods, cx-my-account-v2-notification-preference, cx-my-account-v2-consent-management-form, cx-my-account-v2-consent-management !default; diff --git a/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss b/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss index 7c25cf62726..87fa3a320c8 100644 --- a/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss +++ b/projects/storefrontstyles/scss/components/myaccount/_my-claim-dialog.scss @@ -54,13 +54,12 @@ } .cx-dialog-row--reset-button { - padding:0 12px 0 0 + padding: 0 12px 0 0; } .cx-dialog-row-submit-button { - padding:0 0 0 12px + padding: 0 0 0 12px; } - } } } From 7858833beb7b51cfebe1ba8197b4d4e363f1aa7f Mon Sep 17 00:00:00 2001 From: Cris Li Date: Wed, 18 Dec 2024 17:37:24 +0800 Subject: [PATCH 08/15] CXSPA-9098, fix breaking changes by add feature toggle and inject --- .../feature-toggles/config/feature-toggles.ts | 9 +++++++ .../user/occ-customer-coupon.adapter.ts | 1 + .../customer-coupon.connector.spec.ts | 19 +++++++++++++- .../customer-coupon.connector.ts | 14 +++++++++-- .../cypress/helpers/coupons/my-coupons.ts | 2 +- .../spartacus/spartacus-features.module.ts | 1 + .../claim-dialog/claim-dialog.component.html | 15 +---------- .../claim-dialog.component.spec.ts | 17 +------------ .../claim-dialog/claim-dialog.component.ts | 25 +++++-------------- .../my-coupons/my-coupons.component.ts | 7 +++--- 10 files changed, 54 insertions(+), 56 deletions(-) diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index fda0b5e2f9d..de8572aa6e5 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -778,6 +778,14 @@ export interface FeatureTogglesInterface { */ enableConsecutiveCharactersPasswordRequirement?: boolean; + /** + * In CustomerCouponConnector, Enables claiming customer coupon with coupon code in httpRequest body with POST method. + * + * When set to `false`, claiming customer coupon works with coupon code as parameter in URL, which exposes sensitive data and has security risk. + * When set to `true`, claiming customer coupon works with coupon code in httpRequest body with POST method, which avoids security risk. + */ + enableClaimCustomerCouponWithCodeInRequestBody?: boolean; + /** * Enables a validation that prevents new passwords from matching the current password * in the password update form. @@ -979,4 +987,5 @@ export const defaultFeatureToggles: Required = { useExtendedMediaComponentConfiguration: false, showRealTimeStockInPDP: false, enableSecurePasswordValidation: false, + enableClaimCustomerCouponWithCodeInRequestBody: false, }; diff --git a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts index 36e7057d3ad..115f06a2f5f 100644 --- a/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts +++ b/projects/core/src/occ/adapters/user/occ-customer-coupon.adapter.ts @@ -78,6 +78,7 @@ export class OccCustomerCouponAdapter implements CustomerCouponAdapter { return this.http.post(url, { headers }); } + claimCustomerCouponWithCodeInBody( userId: string, codeVal: string diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts index d95064a99d0..a9ebb43e69f 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.spec.ts @@ -3,6 +3,7 @@ import { of } from 'rxjs'; import { CustomerCouponAdapter } from './customer-coupon.adapter'; import { CustomerCouponConnector } from './customer-coupon.connector'; import createSpy = jasmine.createSpy; +import { FeatureConfigService } from '@spartacus/core'; const PAGE_SIZE = 5; const currentPage = 1; @@ -32,6 +33,7 @@ class MockUserAdapter implements CustomerCouponAdapter { describe('CustomerCouponConnector', () => { let service: CustomerCouponConnector; let adapter: CustomerCouponAdapter; + let featureConfigService: FeatureConfigService; beforeEach(() => { TestBed.configureTestingModule({ @@ -42,6 +44,7 @@ describe('CustomerCouponConnector', () => { service = TestBed.inject(CustomerCouponConnector); adapter = TestBed.inject(CustomerCouponAdapter); + featureConfigService = TestBed.inject(FeatureConfigService); }); it('should be created', () => { @@ -86,8 +89,22 @@ describe('CustomerCouponConnector', () => { ); }); - it('claimCustomerCoupon should call adapter', () => { + it('claimCustomerCoupon should call adapter.claimCustomerCoupon in case enableClaimCustomerCouponWithCodeInRequestBody is disabled', () => { let result; + spyOn(featureConfigService, 'isEnabled').and.returnValue(false); + service + .claimCustomerCoupon('userId', 'couponCode') + .subscribe((res) => (result = res)); + expect(result).toEqual('claim-userId'); + expect(adapter.claimCustomerCoupon).toHaveBeenCalledWith( + 'userId', + 'couponCode' + ); + }); + + it('claimCustomerCoupon should call adapter.claimCustomerCouponWithCodeInBody in case enableClaimCustomerCouponWithCodeInRequestBody is enabled', () => { + let result; + spyOn(featureConfigService, 'isEnabled').and.returnValue(true); service .claimCustomerCoupon('userId', 'couponCode') .subscribe((res) => (result = res)); diff --git a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts index 3db9a168ae0..068899f0311 100644 --- a/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts +++ b/projects/core/src/user/connectors/customer-coupon/customer-coupon.connector.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { CustomerCoupon2Customer, @@ -12,11 +12,13 @@ import { CustomerCouponSearchResult, } from '../../../model/customer-coupon.model'; import { CustomerCouponAdapter } from './customer-coupon.adapter'; +import { FeatureConfigService } from '@spartacus/core'; @Injectable({ providedIn: 'root', }) export class CustomerCouponConnector { + private featureConfigService = inject(FeatureConfigService); constructor(protected adapter: CustomerCouponAdapter) {} getCustomerCoupons( @@ -43,7 +45,15 @@ export class CustomerCouponConnector { userId: string, couponCode: string ): Observable { - return this.adapter.claimCustomerCouponWithCodeInBody(userId, couponCode); + if ( + this.featureConfigService.isEnabled( + 'enableClaimCustomerCouponWithCodeInRequestBody' + ) + ) { + return this.adapter.claimCustomerCouponWithCodeInBody(userId, couponCode); + } else { + return this.adapter.claimCustomerCoupon(userId, couponCode); + } } disclaimCustomerCoupon( diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts index 56e0c34e682..043bba3502b 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/coupons/my-coupons.ts @@ -116,7 +116,7 @@ export function claimCoupon(couponCode: string) { 'getClaimedCouponPage' ); - const claimCoupon = waitForClaimCoupon(couponCode); + const claimCoupon = waitForClaimCouponWithCodeInBody(couponCode); const getCoupons = waitForGetCoupons(); diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index 00dcb926b47..b222ddd35b7 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -418,6 +418,7 @@ if (environment.cpq) { showRealTimeStockInPDP: false, a11yWrapReviewOrderInSection: true, enableSecurePasswordValidation: true, + enableClaimCustomerCouponWithCodeInRequestBody: true, }; return appFeatureToggles; }), diff --git a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html index 7fee7c2aa73..01dc17ded44 100644 --- a/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html +++ b/projects/storefrontlib/cms-components/myaccount/my-coupons/claim-dialog/claim-dialog.component.html @@ -9,20 +9,9 @@