Skip to content

Commit fc35442

Browse files
authored
Add review page handling for B2B OPF Checkout
Closes: CXSPA-9699
1 parent f9642f6 commit fc35442

23 files changed

+574
-63
lines changed

Diff for: integration-libs/opf/_index.scss

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
$opf-components-allowlist: cx-opf-payment-method-details,
1111
cx-opf-checkout-payment-and-review, cx-opf-checkout-payments,
1212
cx-opf-checkout-billing-address-form, cx-opf-checkout-payment-wrapper,
13-
cx-opf-checkout-terms-and-conditions-alert, cx-opf-error-modal,
14-
cx-opf-cta-element, cx-opf-google-pay, cx-opf-apple-pay,
15-
cx-opf-quick-buy-buttons, cx-opf-b2b-checkout-payment-type !default;
13+
cx-opf-checkout-review-card, cx-opf-checkout-terms-and-conditions-alert,
14+
cx-opf-error-modal, cx-opf-cta-element, cx-opf-google-pay, cx-opf-apple-pay,
15+
cx-opf-quick-buy-buttons, cx-opf-b2b-checkout-payment-type,
16+
cx-opf-b2b-checkout-review !default;
1617

1718
$skipComponentStyles: () !default;
1819

Diff for: integration-libs/opf/checkout/assets/translations/en/opfCheckout.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"paymentType": "Method of payment",
55
"shipping": "Shipping",
66
"deliveryMethod": "Delivery Method",
7-
"paymentAndReview": "Payment & Review"
7+
"paymentAndReview": "Payment & Review",
8+
"review": "Review Order"
89
},
910
"paymentAndReviewTitle": "Payment and review",
1011
"billingAddress": "Billing Address",

Diff for: integration-libs/opf/checkout/components/b2b/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66

77
export * from './opf-b2b-checkout-components.module';
88
export * from './opf-b2b-checkout-payment-type/index';
9+
export * from './opf-b2b-checkout-review/index';

Diff for: integration-libs/opf/checkout/components/b2b/opf-b2b-checkout-components.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import { NgModule } from '@angular/core';
88

99
import { OpfB2bCheckoutPaymentTypeModule } from './opf-b2b-checkout-payment-type';
10+
import { OpfB2bCheckoutReviewModule } from './opf-b2b-checkout-review';
1011

1112
@NgModule({
12-
imports: [OpfB2bCheckoutPaymentTypeModule],
13+
imports: [OpfB2bCheckoutPaymentTypeModule, OpfB2bCheckoutReviewModule],
1314
})
1415
export class OpfB2bCheckoutComponentsModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// TODO: Add unit tests

Diff for: integration-libs/opf/checkout/components/b2b/opf-b2b-checkout-payment-type/opf-b2b-checkout-payment-type.module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { OpfB2bCheckoutPaymentTypeComponent } from './opf-b2b-checkout-payment-t
2626
CommonModule,
2727
I18nModule,
2828
SpinnerModule,
29+
FeaturesConfigModule,
30+
OpfCheckoutPaymentsModule,
2931
ConfigModule.withConfig(<CmsConfig>{
3032
cmsComponents: {
3133
OpfCheckoutPaymentType: {
@@ -34,8 +36,6 @@ import { OpfB2bCheckoutPaymentTypeComponent } from './opf-b2b-checkout-payment-t
3436
},
3537
},
3638
}),
37-
FeaturesConfigModule,
38-
OpfCheckoutPaymentsModule,
3939
],
4040
declarations: [OpfB2bCheckoutPaymentTypeComponent],
4141
exports: [OpfB2bCheckoutPaymentTypeComponent],

Diff for: integration-libs/opf/checkout/components/b2b/opf-b2b-checkout-payment-type/opf-b2b-checkout-payment-type.spec.ts

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export * from './opf-b2b-checkout-review.component';
8+
export * from './opf-b2b-checkout-review.module';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<ng-container
2+
*ngIf="{
3+
value: explicitTermsAndConditions$ | async,
4+
} as explicitTermsAndConditions"
5+
>
6+
<div class="cx-opf-review-card-wrapper">
7+
<cx-opf-checkout-review-card
8+
[cardContent$]="getPoNumberCard(poNumber$ | async)"
9+
[editConfig]="{
10+
route: getCheckoutStepUrl(checkoutStepTypePaymentType),
11+
ariaLabelKey: 'checkoutReview.editPoNumber',
12+
}"
13+
></cx-opf-checkout-review-card>
14+
<cx-opf-checkout-review-card
15+
[cardContent$]="
16+
getPaymentMethodNameCard(selectedPaymentProviderName$ | async)
17+
"
18+
[editConfig]="{
19+
route: getCheckoutStepUrl(checkoutStepTypePaymentType),
20+
ariaLabelKey: 'checkoutReview.editPaymentMethod',
21+
}"
22+
></cx-opf-checkout-review-card>
23+
<cx-opf-checkout-review-card
24+
[cardContent$]="getDeliveryAddressCard(deliveryAddress$ | async)"
25+
[editConfig]="{
26+
route: getCheckoutStepUrl(checkoutStepTypeDeliveryAddress),
27+
ariaLabelKey: 'checkoutReview.editDeliveryAddressDetails',
28+
}"
29+
></cx-opf-checkout-review-card>
30+
<cx-opf-checkout-review-card
31+
[cardContent$]="getDeliveryModeCard(deliveryMode$ | async)"
32+
[editConfig]="{
33+
route: getCheckoutStepUrl(checkoutStepTypeDeliveryMode),
34+
ariaLabelKey: 'checkoutReview.editDeliveryMode',
35+
}"
36+
></cx-opf-checkout-review-card>
37+
</div>
38+
39+
<div
40+
*ngIf="explicitTermsAndConditions.value"
41+
class="cx-opf-terms-and-conditions"
42+
[attr.aria-label]="'opfCheckout.termsAndConditions' | cxTranslate"
43+
>
44+
<h3>{{ 'opfCheckout.termsAndConditions' | cxTranslate }}</h3>
45+
46+
<cx-opf-checkout-terms-and-conditions-alert
47+
[isVisible]="termsAndConditionInvalid"
48+
[isExplicit]="explicitTermsAndConditions.value"
49+
[isDismissible]="false"
50+
></cx-opf-checkout-terms-and-conditions-alert>
51+
52+
<form
53+
class="cx-place-order-form form-check"
54+
[formGroup]="checkoutSubmitForm"
55+
>
56+
<div class="form-group">
57+
<label>
58+
<input
59+
formControlName="termsAndConditions"
60+
class="scaled-input form-check-input"
61+
type="checkbox"
62+
(change)="toggleTermsAndConditions()"
63+
/>
64+
<span class="form-check-label">
65+
{{ 'checkoutReview.confirmThatRead' | cxTranslate }}
66+
<a
67+
[routerLink]="{ cxRoute: 'termsAndConditions' } | cxUrl"
68+
class="cx-tc-link"
69+
target="_blank"
70+
rel="noopener noreferrer"
71+
>
72+
{{ 'checkoutReview.termsAndConditions' | cxTranslate }}
73+
</a>
74+
</span>
75+
</label>
76+
</div>
77+
</form>
78+
</div>
79+
80+
<cx-opf-checkout-billing-address-form></cx-opf-checkout-billing-address-form>
81+
82+
<cx-opf-checkout-terms-and-conditions-alert
83+
*ngIf="!explicitTermsAndConditions.value"
84+
[isDismissible]="true"
85+
[isVisible]="termsAndConditionInvalid"
86+
[isExplicit]="explicitTermsAndConditions.value"
87+
></cx-opf-checkout-terms-and-conditions-alert>
88+
89+
<!-- detect if b2c -->
90+
<!--
91+
<cx-opf-checkout-payments
92+
[elementsPerPage]="10"
93+
[explicitTermsAndConditions]="explicitTermsAndConditions.value"
94+
[disabled]="termsAndConditionInvalid"
95+
>
96+
<cx-opf-checkout-terms-and-conditions-alert
97+
*ngIf="!explicitTermsAndConditions.value"
98+
[isDismissible]="true"
99+
[isVisible]="termsAndConditionInvalid"
100+
[isExplicit]="explicitTermsAndConditions.value"
101+
></cx-opf-checkout-terms-and-conditions-alert>
102+
</cx-opf-checkout-payments> -->
103+
104+
<!-- detect for b2b -->
105+
<cx-opf-checkout-payments
106+
[onlyPaymentWrapperMode]="true"
107+
(selectedPaymentProviderName)="onPaymentProviderSelected($event)"
108+
></cx-opf-checkout-payments>
109+
110+
<cx-opf-checkout-review-cart-details
111+
[cart]="cart$ | async"
112+
[entries]="entries$ | async"
113+
></cx-opf-checkout-review-cart-details>
114+
</ng-container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// TODO: Add unit tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
ChangeDetectionStrategy,
9+
Component,
10+
inject,
11+
OnInit,
12+
} from '@angular/core';
13+
import {
14+
UntypedFormBuilder,
15+
UntypedFormGroup,
16+
Validators,
17+
} from '@angular/forms';
18+
import { Cart, PaymentType } from '@spartacus/cart/base/root';
19+
import { CheckoutPaymentTypeFacade } from '@spartacus/checkout/b2b/root';
20+
import { CheckoutReviewSubmitComponent } from '@spartacus/checkout/base/components';
21+
import { CmsService, Page } from '@spartacus/core';
22+
import {
23+
OpfBaseFacade,
24+
OpfMetadataStoreService,
25+
} from '@spartacus/opf/base/root';
26+
import { OPF_EXPLICIT_TERMS_AND_CONDITIONS_COMPONENT } from '@spartacus/opf/checkout/root';
27+
import {
28+
Observable,
29+
take,
30+
map,
31+
filter,
32+
combineLatest,
33+
BehaviorSubject,
34+
} from 'rxjs';
35+
import { Card } from '@spartacus/storefront';
36+
37+
@Component({
38+
selector: 'cx-opf-b2b-checkout-review',
39+
templateUrl: './opf-b2b-checkout-review.component.html',
40+
changeDetection: ChangeDetectionStrategy.OnPush,
41+
standalone: false,
42+
})
43+
export class OpfB2bCheckoutReviewComponent
44+
extends CheckoutReviewSubmitComponent
45+
implements OnInit
46+
{
47+
protected fb = inject(UntypedFormBuilder);
48+
protected opfMetadataStoreService = inject(OpfMetadataStoreService);
49+
protected cmsService = inject(CmsService);
50+
protected checkoutPaymentTypeFacade = inject(CheckoutPaymentTypeFacade);
51+
protected opfBaseFacade = inject(OpfBaseFacade);
52+
53+
protected defaultTermsAndConditionsFieldValue = false;
54+
55+
protected selectedPaymentProviderName$ = new BehaviorSubject<
56+
string | undefined
57+
>(undefined);
58+
59+
explicitTermsAndConditions$: Observable<boolean | undefined> = this.cmsService
60+
.getCurrentPage()
61+
.pipe(
62+
map((page: Page) => {
63+
return this.isCmsComponentInPage(
64+
OPF_EXPLICIT_TERMS_AND_CONDITIONS_COMPONENT,
65+
page
66+
);
67+
})
68+
);
69+
70+
checkoutSubmitForm: UntypedFormGroup = this.fb.group({
71+
termsAndConditions: [
72+
this.defaultTermsAndConditionsFieldValue,
73+
Validators.requiredTrue,
74+
],
75+
});
76+
77+
get termsAndConditionInvalid(): boolean {
78+
return this.checkoutSubmitForm.invalid;
79+
}
80+
81+
get termsAndConditionsFieldValue(): boolean {
82+
return Boolean(this.checkoutSubmitForm.get('termsAndConditions')?.value);
83+
}
84+
85+
get paymentType$(): Observable<PaymentType | undefined> {
86+
return this.activeCartFacade
87+
.getActive()
88+
.pipe(map((cart: Cart) => cart.paymentType));
89+
}
90+
91+
get poNumber$(): Observable<string | undefined> {
92+
return this.checkoutPaymentTypeFacade.getPurchaseOrderNumberState().pipe(
93+
filter((state) => !state.loading && !state.error),
94+
map((state) => state.data)
95+
);
96+
}
97+
98+
getPoNumberCard(poNumber?: string | null): Observable<Card> {
99+
return combineLatest([
100+
this.translationService.translate('opfCheckout.poNumber'),
101+
this.translationService.translate('opfCheckout.noPoNumber'),
102+
]).pipe(
103+
map(([textTitle, noneTextTitle]) => {
104+
return {
105+
title: textTitle,
106+
textBold: poNumber ? poNumber : noneTextTitle,
107+
};
108+
})
109+
);
110+
}
111+
112+
getSelectedPayment$ = this.opfBaseFacade.getActiveConfigurationsState();
113+
114+
getSelectedPaymentId$ = this.opfMetadataStoreService
115+
.getOpfMetadataState()
116+
.pipe(
117+
take(1),
118+
map((data) => data?.selectedPaymentOptionId)
119+
);
120+
121+
getPaymentMethodNameCard(methodName?: string): Observable<Card> {
122+
return combineLatest([
123+
this.translationService.translate('opfCheckout.paymentMethod'),
124+
this.translationService.translate('opfCheckout.noPaymentMethod'),
125+
]).pipe(
126+
map(([title, noPaymentMethod]) => ({
127+
title,
128+
textBold: methodName ?? noPaymentMethod,
129+
text: [],
130+
}))
131+
);
132+
}
133+
134+
protected isCmsComponentInPage(cmsComponentUid: string, page: Page): boolean {
135+
return !!page && JSON.stringify(page).includes(cmsComponentUid);
136+
}
137+
138+
protected updateTermsAndConditionsState() {
139+
this.opfMetadataStoreService.updateOpfMetadata({
140+
termsAndConditionsChecked: this.termsAndConditionsFieldValue,
141+
});
142+
}
143+
144+
toggleTermsAndConditions() {
145+
this.updateTermsAndConditionsState();
146+
}
147+
148+
onPaymentProviderSelected(providerName: string) {
149+
this.selectedPaymentProviderName$.next(providerName);
150+
}
151+
152+
ngOnInit() {
153+
this.updateTermsAndConditionsState();
154+
}
155+
}

0 commit comments

Comments
 (0)