-
Notifications
You must be signed in to change notification settings - Fork 395
feat: initial structure for components and b2b flow #20103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
7e38c0c
9f20db4
6ed64a9
4673f5c
e5dfcba
200d681
ca9c724
f26b3c5
c099447
a47f2a8
538b4ae
08c4578
f7523b4
65e7734
72d1b35
dd9b9cb
65b0ace
4ad293d
c22915a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export * from './opf-b2b-checkout-components.module'; | ||
export * from './opf-b2b-checkout-payment-type/index'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { NgModule } from '@angular/core'; | ||
|
||
import { OpfB2bCheckoutPaymentTypeModule } from './opf-b2b-checkout-payment-type'; | ||
|
||
@NgModule({ | ||
imports: [OpfB2bCheckoutPaymentTypeModule], | ||
}) | ||
export class OpfB2bCheckoutComponentsModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export * from './opf-b2b-checkout-payment-type.component'; | ||
export * from './opf-b2b-checkout-payment-type.module'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<h2 class="cx-checkout-title d-none d-lg-block d-xl-block"> | ||
{{ 'checkoutB2B.methodOfPayment.paymentType' | cxTranslate }} | ||
</h2> | ||
<ng-container | ||
*ngIf="(typeSelected$ | async) && !(isUpdating$ | async); else loading" | ||
> | ||
<ng-container> | ||
<div | ||
*cxFeature="'!a11yRemoveStatusLoadedRole'" | ||
role="status" | ||
[attr.aria-label]="'common.loaded' | cxTranslate" | ||
></div> | ||
<div class="row"> | ||
<div class="col-md-12 col-lg-6"> | ||
<label> | ||
<span class="label-content">{{ | ||
'checkoutB2B.poNumber' | cxTranslate | ||
}}</span> | ||
<input | ||
#poNumber | ||
class="form-control" | ||
formControlName="poNumber" | ||
type="text" | ||
placeholder="{{ 'checkoutB2B.placeholder' | cxTranslate }}" | ||
value="{{ cartPoNumber$ | async }}" | ||
/> | ||
</label> | ||
</div> | ||
</div> | ||
|
||
<cx-opf-checkout-payments | ||
[isHeadingDisplayed]="false" | ||
[isPaymentRenderBelow]="false" | ||
[isPaymentInfoMessageEnabled]="false" | ||
(paymentChange)="handlePaymentChange($event)" | ||
></cx-opf-checkout-payments> | ||
|
||
<div class="cx-checkout-btns row"> | ||
<div class="col-md-12 col-lg-6"> | ||
<button class="btn btn-block btn-secondary" (click)="back()"> | ||
{{ 'checkout.backToCart' | cxTranslate }} | ||
</button> | ||
</div> | ||
<div class="col-md-12 col-lg-6"> | ||
<button class="btn btn-block btn-primary" (click)="next()"> | ||
{{ 'common.continue' | cxTranslate }} | ||
</button> | ||
</div> | ||
</div> | ||
</ng-container> | ||
</ng-container> | ||
<ng-template #loading> | ||
<div class="cx-spinner" *ngIf="!paymentTypesError"> | ||
<cx-spinner></cx-spinner> | ||
</div> | ||
</ng-template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
/* | ||
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]> | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { | ||
ChangeDetectionStrategy, | ||
Component, | ||
ElementRef, | ||
inject, | ||
ViewChild, | ||
} from '@angular/core'; | ||
import { ActivatedRoute } from '@angular/router'; | ||
import { PaymentType } from '@spartacus/cart/base/root'; | ||
import { | ||
B2BPaymentTypeEnum, | ||
CheckoutPaymentTypeFacade, | ||
} from '@spartacus/checkout/b2b/root'; | ||
import { CheckoutStepService } from '@spartacus/checkout/base/components'; | ||
import { CheckoutStepType } from '@spartacus/checkout/base/root'; | ||
import { | ||
getLastValueSync, | ||
GlobalMessageService, | ||
GlobalMessageType, | ||
HttpErrorModel, | ||
isNotUndefined, | ||
OccHttpErrorType, | ||
} from '@spartacus/core'; | ||
import { | ||
OpfActiveConfiguration, | ||
OpfBaseFacade, | ||
} from '@spartacus/opf/base/root'; | ||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; | ||
import { | ||
catchError, | ||
distinctUntilChanged, | ||
filter, | ||
map, | ||
tap, | ||
} from 'rxjs/operators'; | ||
|
||
@Component({ | ||
selector: 'cx-opf-b2b-checkout-payment-type', | ||
templateUrl: './opf-b2b-checkout-payment-type.component.html', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
standalone: false, | ||
}) | ||
export class OpfB2bCheckoutPaymentTypeComponent { | ||
protected opfBaseService = inject(OpfBaseFacade); | ||
@ViewChild('poNumber', { static: false }) | ||
private poNumberInputElement: ElementRef<HTMLInputElement>; | ||
|
||
protected busy$ = new BehaviorSubject<boolean>(false); | ||
|
||
typeSelected?: string; | ||
paymentTypesError = false; | ||
|
||
isUpdating$ = combineLatest([ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seeing a number of methods without return type, fee free to add them even though in this it makes sens it is a boolean form method name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code will be evaluating, but I will add all needed typing for the final version. |
||
this.busy$, | ||
this.checkoutPaymentTypeFacade | ||
.getSelectedPaymentTypeState() | ||
.pipe(map((state) => state.loading)), | ||
]).pipe( | ||
map(([busy, loading]) => busy || loading), | ||
distinctUntilChanged() | ||
); | ||
|
||
paymentTypes$: Observable<OpfActiveConfiguration[]> = | ||
this.checkoutPaymentTypeFacade.getPaymentTypes().pipe( | ||
tap(() => (this.paymentTypesError = false)), | ||
catchError((error: HttpErrorModel) => { | ||
if ( | ||
error.details?.[0]?.type === OccHttpErrorType.CLASS_MISMATCH_ERROR | ||
) { | ||
this.globalMessageService.add( | ||
{ key: 'httpHandlers.forbidden' }, | ||
GlobalMessageType.MSG_TYPE_ERROR | ||
); | ||
this.paymentTypesError = true; | ||
} | ||
return of([]); | ||
}) | ||
); | ||
|
||
typeSelected$: Observable<PaymentType> = combineLatest([ | ||
this.checkoutPaymentTypeFacade.getSelectedPaymentTypeState().pipe( | ||
filter((state) => !state.loading), | ||
map((state) => state.data) | ||
), | ||
this.paymentTypes$, | ||
]).pipe( | ||
map( | ||
([selectedPaymentType, availablePaymentTypes]: [ | ||
PaymentType | undefined, | ||
PaymentType[], | ||
]) => { | ||
if ( | ||
selectedPaymentType && | ||
availablePaymentTypes.find((availablePaymentType) => { | ||
return availablePaymentType.code === selectedPaymentType.code; | ||
}) | ||
) { | ||
return selectedPaymentType; | ||
} | ||
if (availablePaymentTypes.length) { | ||
this.busy$.next(true); | ||
this.checkoutPaymentTypeFacade | ||
.setPaymentType( | ||
availablePaymentTypes[0].code as string, | ||
this.poNumberInputElement?.nativeElement?.value | ||
) | ||
.subscribe({ | ||
complete: () => this.onSuccess(), | ||
error: () => this.onError(), | ||
}); | ||
return availablePaymentTypes[0]; | ||
} | ||
return undefined; | ||
} | ||
), | ||
filter(isNotUndefined), | ||
distinctUntilChanged(), | ||
tap((selected) => { | ||
this.typeSelected = selected?.code; | ||
this.checkoutStepService.disableEnableStep( | ||
CheckoutStepType.PAYMENT_DETAILS, | ||
selected?.code === B2BPaymentTypeEnum.ACCOUNT_PAYMENT | ||
); | ||
this.checkoutStepService.disableEnableStep( | ||
CheckoutStepType.REVIEW_ORDER, | ||
selected?.code === B2BPaymentTypeEnum.CARD_PAYMENT | ||
); | ||
}) | ||
); | ||
|
||
cartPoNumber$: Observable<string> = this.checkoutPaymentTypeFacade | ||
.getPurchaseOrderNumberState() | ||
.pipe( | ||
filter((state) => !state.loading), | ||
map((state) => state.data), | ||
filter(isNotUndefined), | ||
distinctUntilChanged() | ||
); | ||
|
||
constructor( | ||
protected checkoutPaymentTypeFacade: CheckoutPaymentTypeFacade, | ||
protected checkoutStepService: CheckoutStepService, | ||
protected activatedRoute: ActivatedRoute, | ||
protected globalMessageService: GlobalMessageService | ||
) {} | ||
|
||
changeType(code: string): void { | ||
this.busy$.next(true); | ||
this.typeSelected = code; | ||
|
||
this.checkoutPaymentTypeFacade | ||
.setPaymentType(code, this.poNumberInputElement?.nativeElement.value) | ||
.subscribe({ | ||
complete: () => this.onSuccess(), | ||
error: () => this.onError(), | ||
}); | ||
} | ||
|
||
next(): void { | ||
if (!this.typeSelected) { | ||
return; | ||
} | ||
|
||
const poNumberInput = this.poNumberInputElement?.nativeElement.value; | ||
// if the PO number didn't change | ||
if (poNumberInput === getLastValueSync(this.cartPoNumber$)) { | ||
this.checkoutStepService.next(this.activatedRoute); | ||
return; | ||
} | ||
|
||
this.busy$.next(true); | ||
this.checkoutPaymentTypeFacade | ||
.setPaymentType(this.typeSelected, poNumberInput) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to unsubscribe or we are 100% sure it complete after 1 time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I will fix that. |
||
.subscribe({ | ||
// we don't call onSuccess here, because it can cause a spinner flickering | ||
complete: () => this.checkoutStepService.next(this.activatedRoute), | ||
error: () => this.onError(), | ||
}); | ||
} | ||
|
||
back(): void { | ||
this.checkoutStepService.back(this.activatedRoute); | ||
} | ||
|
||
protected onSuccess(): void { | ||
this.busy$.next(false); | ||
} | ||
|
||
protected onError(): void { | ||
this.busy$.next(false); | ||
} | ||
|
||
handlePaymentChange(payment: OpfActiveConfiguration) { | ||
if (payment.merchantId === 'B2B_ACCOUNT') { | ||
this.changeType(B2BPaymentTypeEnum.ACCOUNT_PAYMENT); | ||
} else { | ||
this.changeType(B2BPaymentTypeEnum.CARD_PAYMENT); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it affect perf to has a sync directly in form attribute?
wondering if we should subscribe it once in 'div' or 'container'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it may affect performance. I will try to fix this.