Dashboard Notifications & CTAs

Dashboard notifications are the primary mechanism for surfacing offers, required actions and announcements. The server decides what to show; the client decides how — a CTA card on the dashboard, or a modal opened automatically at session start.

Notification types

export enum DashboardNotificationType {
    InstantAdvance          = 'instant_advance',
    InstantTransfer         = 'instant_transfer',
    OpenInvoices            = 'open_invoices',
    SellerCard              = 'seller_card',
    PlaidCollection         = 'collection_account',
    PlaidAdvance            = 'advance_underwriting',
    PreAdvanceLine          = 'pre_advance_line',
    AdvanceLine             = 'advance_line',
    LongTermContract        = 'new_long_term_contract',
    LongTermContractRenewal = 'renew_long_term_contract',
    WelcomeMessage          = 'new_sd_welcome',
    SupplierSuspension      = 'supplier_suspension',
    SellerCardAd            = 'seller_card_transfer_ad',
    PreInstantAdvance       = 'pre_instant_advance',
    CancellationSurvey      = 'cancellation_survey',
    CrossSell               = 'product_cross_sell_page',
    AirwallexPcardTerms     = 'airwallex_pcard_terms',
    IdentityDocumentExpired = 'identity_document_expired',
}

The Notification model

src/app/core/auth/_models/notifications.model.ts

export class Notification<D = any> {
    notificationSupplierKey: string;
    supplierKey: string;
    notificationType: DashboardNotificationType;
    externalProductId?: any;
    startDate: string;
    expirationDate?: any;
    hidden: boolean;
    declined: boolean;
    declineLoading: boolean;
    acceptLoading: boolean;
    visited: boolean;
    seen: boolean;
    inactivated: boolean;
    data: D;                  (1)
    successShow: boolean;
    firstSeen: boolean;
    actionLoading: boolean;
    additional_data?: any;
}
1 Typed per notification. LTC notifications carry DashboardNotificationLtcDataDto[], the identity notification carries IdentityDocumentExpiredDataDto, the suspension notification carries SuspensionNotificationDataDto.

DashboardNotifications.fromDto() maps the flat API payload onto named fields:

API key Model field

collection_account

plaidCollection

advance_underwriting

advanceUnderwriting

instant_transfer

instantTransfer

open_invoices

openInvoices

seller_card

sellerCard

instant_advance

instantAdvance

pre_advance_line

preAdvanceLine

advance_line

advanceLine

new_long_term_contract

longTermContract

renew_long_term_contract

longTermContractRenewal

new_sd_welcome

welcomeMessage

item_login_required

itemLoginRequired

pre_instant_advance

preInstantAdvance

supplier_suspension

supplierSuspension

seller_card_transfer_ad

sellerCardAd

cancellation_survey

cancellationSurvey

product_cross_sell_page

crossSell

airwallex_pcard_terms

airwallexPcardTerms

identity_document_expired

identityDocumentExpired

airwallex_pcard_terms and identity_document_expired are read through (dto as any) because DashboardNotificationsDto in @payability/pyb-ng2-api does not expose them yet. Both are marked with a TODO to remove the cast once the API package catches up.

itemLoginRequired is mapped from the DTO but has no corresponding DashboardNotificationType member — it is currently read-only state.

Actions on a notification

export enum DashboardNotificationAction {
    Visited        = 'visited',
    Hide           = 'hide',
    Show           = 'show',
    Decline        = 'decline',
    DeclineLoading = 'declineLoading',
    Accept         = 'accept',
    AcceptLoading  = 'acceptLoading',
    ActionLoading  = 'actionLoading',
}

Every state-changing action follows the same effect shape: call notificationService.callNotificationAction(supplierKey, type, action), then dispatch updatedDashboardNotifications({ type, action, value }) to reflect it locally.

visitedNotification$ = createEffect(() =>
    this.actions$.pipe(
        ofType(DashNotificationActions.visitedDashboardNotifications),
        withLatestFrom(this.store.select(selectCurrentUserId)),
        switchMap(([{ payload }, supplierKey]) => {
            const type = (payload as ActionsDashboardNotificationPayload).type;
            return zip(of(type),
                this.notificationService.callNotificationAction(supplierKey, type, DashboardNotificationAction.Visited));
        }),
        map(([type]) => DashNotificationActions.updatedDashboardNotifications({
            payload: { type, action: DashboardNotificationAction.Visited, value: true },
        })),
    ),
);

Accept

acceptDashboardNotificationsSuccess opens a success modal whose copy is chosen by notification type:

map(({ payload: { type, silent } }) => {
    if (!silent) {
        const phraseKeyByType: Partial<Record<DashboardNotificationType, string>> = {
            [DashboardNotificationType.PreAdvanceLine]:      'CTA.PRE_ADVANCE_LINE.SUCCESS',
            [DashboardNotificationType.AirwallexPcardTerms]: 'CTA.AIRWALLEX_PCARD_TERMS.SUCCESS',
        };
        const phraseKey = phraseKeyByType[type] || 'CTA.PRE_INSTANT_ADVANCE.SUCCESS';   (1)

        this.translateService.get(phraseKey).subscribe(({ HEADER: title, DESCRIPTION: description }) => {
            this.messageModalService.openSuccessModal({ title, description });
        });
    }
    return DashNotificationActions.fetchDashboardNotifications();   (2)
}),
1 Any type not in the map falls back to the Pre Instant Advance copy. Adding a notification that is accepted without adding a mapping produces a plausible-looking but wrong success message.
2 The whole set is refetched after every accept, so server-side follow-on changes are picked up.

silent: true suppresses the modal entirely — used when a single dialog resolves several notifications at once, as in the Seller Card transition.

CTA cards

views/partials/content/cta/ renders notifications as dismissible dashboard cards. CtaFactory maps a notification type onto a strategy that produces an IAdCard configuration.

public getCtaConfig(type: DashboardNotificationType, notification: Notification): IAdCard {
    switch (type) {
        case DashboardNotificationType.PreAdvanceLine:    return new CtaPreAdvanceLineStrategy(…).generate();
        case DashboardNotificationType.AdvanceLine:       return new CtaAdvanceLineStrategy(…).generate();
        case DashboardNotificationType.LongTermContract:  return new CtaLongTermContractStrategy(…).generate();
        case DashboardNotificationType.InstantAdvance:    return new CtaInstantAdvanceStrategy(…).generate();
        case DashboardNotificationType.PreInstantAdvance: return new CtaPreInstantAdvanceStrategy(…).generate();
        case DashboardNotificationType.CrossSell:         return new CtaCrossSellStrategy(…).generate();
        default: return null;
    }
}

Active strategies

CtaPreAdvanceLineStrategy

Pre-qualified Advance Line offer.

CtaAdvanceLineStrategy

Live Advance Line offer.

CtaLongTermContractStrategy

New LTC offer → LtcFlowService.

CtaInstantAdvanceStrategy

Instant Advance offer.

CtaPreInstantAdvanceStrategy

Pre-qualified Instant Advance offer.

CtaCrossSellStrategy

Product cross-sell page.

Disabled strategies

These exist in the repository but are commented out of the factory:

CtaSellerCardStrategy

Seller Card offer.

CtaInstantTransferStrategy

Instant Transfer offer.

CtaOpenInvoicesStrategy

Open invoices.

CtaLongTermContractRenewalStrategy

LTC renewal — disabled deliberately per CA-513.

CtaFactory.getCtaConfig has a duplicate case DashboardNotificationType.AdvanceLine — the second is unreachable. Harmless, but do not copy the pattern.

Supporting pieces:

cta-strategy.ts

The abstract strategy base.

cta-long-term-contract-abstract.strategy.ts

Shared LTC behaviour for the new and renewal strategies.

cta-session-keys.ts

Session-storage keys for per-session dismissal.

open-invoices/

A dedicated OpenInvoicesCtaComponent outside the strategy system.

The user’s response is expressed with CtaActionTypesEnum:

export enum CtaActionTypesEnum {
    Accept       = 'accept',
    Decline      = 'decline',
    SaveForLater = 'save',
}

Auto-opened modals

Three notifications bypass the card system and open a modal directly from GlobalSubscriptionsService.init(). Each subscribes with take(1), so it fires at most once per session.

new_sd_welcome

WelcomeModalComponent. On close, dispatches declineDashboardNotifications — seen once, never again.

airwallex_pcard_terms

AirwallexPcardTermsModalComponent. Only Accept resolves it; dismissing leaves it to reappear.

identity_document_expired

SellerCardTransitionModalComponent, which folds in the terms step when needed and launches Entrust on accept.

The identity notification suppresses the standalone terms dialog:

filter(notifications =>
    this.canSeeAirwallexPcardTerms(notifications?.airwallexPcardTerms) &&
    !this.canSeeIdentityDocumentExpired(notifications?.identityDocumentExpired)),

"Can see" means the notification exists and declined is false.

See Seller Card for the full transition flow.

Notifications that drive flows

new_long_term_contract

LtcFlowService — see Long Term Contract.

renew_long_term_contract

LtcRenewalFlowService.

cancellation_survey

The cancellation survey — see Cancellation & Reactivation.

supplier_suspension

Suspension messaging — see Suspension.

collection_account / advance_underwriting

Plaid linking prompts — see Plaid.

open_invoices

enableOpenInvoices → OpenInvoicesService.enableOpenInvoices() then a CTA.OPEN_INVOICES.SUCCESS modal.

Permission gating

The notification feed on the dashboard requires VIEW_NOTIFICATIONS, which is denied to pending-and-expedited suppliers. The auto-opened modals in GlobalSubscriptionsService are not permission-gated — they run for anyone whose notification set contains them.

Clearing

clearDashboardNotifications() empties the slice locally without an API call. It is dispatched at the start of most DocuSign handlers so stale offers disappear immediately, before fetchDashboardNotifications() brings back the refreshed set.