libs/pfe-connector/src/lib/frame/pfe-frame-navigation.service.ts

Extends

TalyFrameNavigationService

Relationships

Used by

No results matching.

Index

Properties
Methods

Constructor

constructor()

Methods

cancel
cancel()
Returns : void
getOfferCode$
getOfferCode$(stateKey: string)
Parameters :
Name Type Optional
stateKey string No
gotoErrorPage
gotoErrorPage(errorResponse?: HttpErrorResponse)

Method to navigate to the error page

Parameters :
Name Type Optional
errorResponse HttpErrorResponse Yes
Returns : void
gotoPage
gotoPage(pageId: string, rewindHistory?: boolean)
Parameters :
Name Type Optional
pageId string No
rewindHistory boolean Yes
Returns : void
gotoSection
gotoSection(sectionId: string, rewindHistory?: boolean)

Method to navigate to a particular section with given section id

Implement this function. It might not be always the first page of the section. It might change based on the conditions in pfe navigation configuration as well.

Parameters :
Name Type Optional
sectionId string No
rewindHistory boolean Yes
Returns : void
init
init()
Returns : void
initApplicationBusy$
initApplicationBusy$()
Returns : void
initApplicationBusyMessage$
initApplicationBusyMessage$(busy$: Observable)
Parameters :
Name Type Optional
busy$ Observable<boolean> No
Returns : void
navigateBack
navigateBack()
Returns : void
Async navigateHome
navigateHome()
Returns : any
Async navigateNext
navigateNext()
Returns : any
saveOffer
saveOffer()
Returns : void
Public disableTitleUpdates
disableTitleUpdates()
Returns : void
Public setSections
setSections(sections: SectionConfig)
Parameters :
Name Type Optional Default value
sections SectionConfig No new Map()
Returns : void
Public setTranslatedTitle
setTranslatedTitle(translatedTitle: string)
Parameters :
Name Type Optional
translatedTitle string No
Returns : void

Properties

Readonly APPLICATION_BUSY_DELAY
Type : number
Default value : 1000
applicationBusy$
Type : Observable<boolean>
applicationBusyMessage$
Type : Observable<ExtendedDisplayMessage | undefined>
Readonly backClicked$
Type : unknown
Default value : this.backClickedSubject.asObservable()
Readonly MINIMUM_MESSAGE_TIME
Type : number
Default value : 1000
navigationConfig
Type : unknown
Default value : inject<WritableSignal<NavigationConfig>>(TALY_FRAME_NAVIGATION_CONFIG)
Readonly nextClicked$
Type : unknown
Default value : this.nextClickedSubject.asObservable()
pageActionStatus$
Type : Observable<PageActionStatus>
currentSectionStates$
Type : unknown
Default value : this._currentSectionStates$.asObservable()
history$
Type : unknown
Default value : this._history$.asObservable()
visibleSections$
Type : unknown
Default value : this._visibleSections$.asObservable()
import {
  ExpressionCondition,
  ExtendedDisplayMessage,
  JSON_PATH_REGEX,
  PfeActionsService,
  PfeBusinessService,
  PfeConditionsService,
  PfeConfigurationService,
  PfeNavigationService,
  PfeNavigationUtilService,
  PfeServiceActivatorService,
  PfeStateService,
  ServiceActivatorProgressDetails
} from '@allianz/ngx-pfe';
import {
  NavigationConfig,
  PageActionStatus,
  PageHistory,
  SectionConfig,
  TALY_FRAME_NAVIGATION_CONFIG,
  TalyFrameNavigationService
} from '@allianz/taly-core/frame';
import { BackLinkUtilsService } from '@allianz/taly-core/web-components';
import { HttpErrorResponse } from '@angular/common/http';
import {
  ApplicationRef,
  effect,
  inject,
  Injectable,
  Renderer2,
  RendererFactory2,
  WritableSignal
} from '@angular/core';
import { BehaviorSubject, combineLatest, EMPTY, merge, Observable, of, Subject, timer } from 'rxjs';
import {
  debounce,
  debounceTime,
  distinctUntilChanged,
  filter,
  map,
  switchMap,
  take,
  tap,
  timeInterval
} from 'rxjs/operators';
import { createPageActionStatusObservable } from './page-action-status';
import { TalyStickyService, TalyPageDataService, TalyPageService } from '@allianz/taly-core';

export const PFE_HISTORY_KEY = 'pfeHistory';

@Injectable()
export class PfeFrameNavigationService extends TalyFrameNavigationService {
  private pfeBusinessService = inject(PfeBusinessService);
  private pfeConditionsService = inject(PfeConditionsService);
  private pfeConfigurationService = inject(PfeConfigurationService);
  private pfeStateService = inject(PfeStateService);
  private pfeServiceActivatorService = inject(PfeServiceActivatorService);
  private pfeNavigationService = inject(PfeNavigationService);
  private talyPageService = inject(TalyPageService);
  private talyPageDataService = inject(TalyPageDataService);
  navigationConfig = inject<WritableSignal<NavigationConfig>>(TALY_FRAME_NAVIGATION_CONFIG);
  private backLinkUtilsService = inject(BackLinkUtilsService);
  private actionsService = inject(PfeActionsService);
  private pfeNavigationUtilService = inject(PfeNavigationUtilService);

  pageActionStatus$!: Observable<PageActionStatus>;
  applicationBusy$!: Observable<boolean>;
  applicationBusyMessage$!: Observable<ExtendedDisplayMessage | undefined>;

  readonly APPLICATION_BUSY_DELAY = 1000;
  readonly MINIMUM_MESSAGE_TIME = 1000;

  private readonly nextClickedSubject = new Subject<void>();
  readonly nextClicked$ = this.nextClickedSubject.asObservable();

  private readonly backClickedSubject = new Subject<void>();
  readonly backClicked$ = this.backClickedSubject.asObservable();

  private nextButtonDisabledSubject = new BehaviorSubject<boolean>(false);

  private applicationRef = inject(ApplicationRef);
  private talyStickyService = inject(TalyStickyService);
  private renderer!: Renderer2;
  private rendererFactory = inject(RendererFactory2);

  constructor() {
    super();
    this.init();
    this.renderer = this.rendererFactory.createRenderer(null, null);
    effect(() => {
      this.nextButtonDisabledSubject.next(
        this.navigationConfig().nextButtonDisabledWhenPageInvalid ?? false
      );
    });
  }

  init() {
    createPfeHistoryObservable(this.pfeStateService)
      .pipe(
        tap((value) => {
          this.updateHistory(value.history);
        })
      )
      .subscribe();

    this.pageActionStatus$ = this.nextButtonDisabledSubject.pipe(
      switchMap((nextButtonDisabledWhenPageInvalid) =>
        createPageActionStatusObservable(
          this.pfeServiceActivatorService,
          this.pfeNavigationService,
          this.pfeConfigurationService,
          this.pfeBusinessService,
          this.backLinkUtilsService,
          this.talyPageDataService,
          nextButtonDisabledWhenPageInvalid
        )
      )
    );

    this.initApplicationBusy$();
  }

  async navigateNext() {
    this.nextClickedSubject.next();

    function hasErrorNotification() {
      return document.querySelector('.error-notification-wrapper');
    }

    const scrollToTopOfFrame = () => {
      const frameContent = document.querySelector('#frameContent');
      if (frameContent) {
        const scrollMargin = this.talyStickyService.getStickyElementHeights();
        this.renderer.setStyle(frameContent, 'scroll-margin-top', `${scrollMargin}px`);
        frameContent.scrollIntoView({ behavior: 'smooth' });
      }
    };

    // WHEN PAGE IS NOT COMPLETE
    if (!this.pfeBusinessService.pageStatus$.value) {
      if (this.navigationConfig().nextButtonDisabledWhenPageInvalid) {
        return;
      }

      if (hasErrorNotification()) {
        scrollToTopOfFrame();
        return;
      }

      this.talyPageService.onNextPageRequested();
      return;
    }

    // WHEN PAGE IS COMPLETE
    // forward the option doNotActuallyNavigate = true to prevent PFE from navigating
    // the actual navigation will be done once TALY makes sure that there is no error notification on a page
    // the pfeNavigationService will take care of executing the "onPageLeave" service activator and action.
    const pageConfig = await this.pfeNavigationService.navigateNext(true);

    // trigger the notification component to re-render after the actions are executed
    this.applicationRef.tick();
    if (hasErrorNotification()) {
      scrollToTopOfFrame();

      // Normally, PFE takes care of sending the `navigationInProgress$.next(false)` when a navigation is success or cancelled
      // But now that we handle the navigation logic within TALY, this has to be taken care of manually
      // Otherwise, the spinner will keep running and block the entire page.
      this.pfeNavigationService.navigationInProgress$.next(false);
      return;
    }

    // PFE returns no page config when the navigation was intentionally stopped
    // (e.g. an "onPageLeave" action performed an external redirect like OAUTH_AUTHORIZE).
    // Genuine errors are already handled by PFE before it returns, so we just stop here
    // instead of calling navigateRelative with an undefined page id (which would throw NG04008 error).
    if (!pageConfig?.pageId) {
      return;
    }

    this.pfeNavigationUtilService.navigateRelative(pageConfig.pageId);
  }

  navigateBack() {
    this.backClickedSubject.next();
    this.pfeBusinessService.navigateBack();
  }

  async navigateHome() {
    const firstPageId = await this.pfeBusinessService.getFirstPage();
    if (!firstPageId) {
      throw new Error('Trying to navigate to the Homepage: PFE returned an undefined first page.');
    }

    this.gotoPage(firstPageId);
  }

  gotoPage(pageId: string, rewindHistory?: boolean): void {
    if (this.pfeBusinessService.currentPageId$.value === pageId) {
      return;
    }
    if (!rewindHistory) {
      this.pfeBusinessService.navigateToPage(pageId);
    } else {
      const rewindHistoryAction = {
        type: 'PFE_REWIND_HISTORY',
        pageId
      };
      this.actionsService.executeAction(rewindHistoryAction).then(() => {
        this.pfeBusinessService.navigateToPage(pageId);
      });
    }
  }

  /**
   * Method to navigate to a particular section with given section id
   *
   * @todo Implement this function. It might not be always the first
   * page of the section. It might change based on the conditions in
   * pfe navigation configuration as well.
   */
  gotoSection(sectionId: string, rewindHistory?: boolean): void {
    const sections = this._visibleSections$.value;
    const section = sections.get(sectionId);
    const pages = section?.pages || [];

    if (pages?.length === 0) {
      throw new Error(
        `Could not find proper configuration to navigate to the section ${sectionId}`
      );
    }

    // Navigating to first page of the section
    this.gotoPage(pages[0], rewindHistory);
  }

  /**
   * Method to navigate to the error page
   *
   */
  gotoErrorPage(errorResponse?: HttpErrorResponse) {
    this.pfeBusinessService.navigateToErrorPage(errorResponse);
  }

  saveOffer(): void {
    this.actionsService.executeAction({
      type: 'TALY_SAVE_OFFER'
    });
  }

  cancel(): void {
    this.actionsService.executeAction({
      type: 'TALY_CANCEL_OPERATION'
    });
  }

  getOfferCode$(stateKey: string): Observable<string | undefined> {
    return this.pfeBusinessService.getObservableForExpressionKey(stateKey, true);
  }

  initApplicationBusy$(): void {
    const busy$: Observable<boolean> = combineLatest([
      this.pfeServiceActivatorService.serviceActivatorCallInProgress$,
      this.pfeNavigationService.navigationInProgress$
    ]).pipe(
      map(
        ([serviceActivatorCallInProgress, navigationInProgress]) =>
          serviceActivatorCallInProgress || navigationInProgress
      ),
      distinctUntilChanged()
    );
    this.applicationBusy$ = busy$.pipe(
      debounce((busy) => (busy ? timer(this.APPLICATION_BUSY_DELAY) : timer(0)))
    );

    this.initApplicationBusyMessage$(busy$);
  }

  initApplicationBusyMessage$(busy$: Observable<boolean>): void {
    const message$ = this.pfeServiceActivatorService.serviceActivatorCallInProgressDetails$.pipe(
      filter((configs: ServiceActivatorProgressDetails[]) => Boolean(configs?.length)),
      map(
        (configs: ServiceActivatorProgressDetails[]) =>
          // If multiple service activators are running at the same time
          // Use the first one that has displayMessage config
          configs.find((config) => config.displayMessage)?.displayMessage
      )
    );

    // Emit a new message every 1 second
    const minimumDisplay$ = timer(0, this.MINIMUM_MESSAGE_TIME).pipe(
      switchMap(() => message$.pipe(take(1)))
    );

    // Switch a message if it's been displayed longer than 1 second
    const messageSwitcher$ = message$.pipe(
      timeInterval(),
      filter(({ interval }) => interval > this.MINIMUM_MESSAGE_TIME),
      map(({ value }) => value)
    );

    const applicationBusyMessage$ = merge(minimumDisplay$, messageSwitcher$).pipe(
      distinctUntilChanged()
    );

    this.applicationBusyMessage$ = busy$.pipe(
      switchMap((isBusy) => {
        if (isBusy) {
          return applicationBusyMessage$;
        }
        return EMPTY;
      })
    );
  }

  protected getVisibleSections$(sections: SectionConfig): Observable<SectionConfig> {
    const conditionalSections = Array.from(sections).filter(
      ([, section]) => section.visibleIf !== undefined
    );

    if (conditionalSections.length === 0) {
      return of(sections);
    }

    const conditions$: Observable<[sectionId: string, stateValue: boolean]>[] =
      conditionalSections.map(([sectionId, section]) => {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        if (JSON_PATH_REGEX.test(section.visibleIf!)) {
          return this.createSectionObservableWithPfeExpressionCondition(
            sectionId,
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            section.visibleIf!
          );
        } else {
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          return this.createSectionObservableWithJsonPathExpression(sectionId, section.visibleIf!);
        }
      });

    return combineLatest(conditions$).pipe(
      map((conditions) => {
        const conditionMap = new Map(conditions);
        return filterSections(sections, conditionMap);
      })
    );
  }

  private createSectionObservableWithPfeExpressionCondition(
    sectionId: string,
    visibleIf: string
  ): Observable<[sectionId: string, stateValue: boolean]> {
    const condition: ExpressionCondition[] = [{ expression: visibleIf }];
    // get all property keys which are in curly brackets (e.g. "{key1} == {key2}" => ["key1", "key2"])
    const propertyKeys = visibleIf.match(JSON_PATH_REGEX)?.map((match) => match.slice(1, -1)) || [];

    return merge(
      ...propertyKeys.map((key) => this.pfeBusinessService.getObservableForExpressionKey(key, true))
    ).pipe(
      map(() => [
        sectionId,
        this.pfeConditionsService.evaluateConditions(condition, this.pfeStateService.getFullState())
      ])
    );
  }

  private createSectionObservableWithJsonPathExpression(
    sectionId: string,
    visibleIf: string
  ): Observable<[sectionId: string, stateValue: boolean]> {
    return this.pfeBusinessService
      .getObservableForExpressionKey(visibleIf, true)
      .pipe(map((stateValue) => [sectionId, Boolean(stateValue)]));
  }
}

function createPfeHistoryObservable(pfeStateService: PfeStateService): Observable<PageHistory> {
  return pfeStateService.getObservableForKey(PFE_HISTORY_KEY).pipe(
    debounceTime(50),
    map((history) => {
      let currentPage = null;

      if (history.length > 0) {
        currentPage = history[history.length - 1];
      }
      return { history, currentPage };
    })
  );
}

function filterSections(
  allSections: SectionConfig,
  conditions: Map<string, boolean>
): SectionConfig {
  return new Map(
    Array.from(allSections).filter(([sectionId]) => {
      const hasCondition = conditions.has(sectionId);
      const conditionIsTrue = conditions.get(sectionId) === true;
      return !hasCondition || conditionIsTrue;
    })
  );
}

results matching ""

    No results matching ""