Iframe messaging and EVA
What EVA is
EVA is a separate Payability front end. Parts of Internal Admin are not rendered by
this codebase at all: they are EVA pages loaded in iframes, integrated over a typed
postMessage protocol.
This is a deliberate architectural seam — see decision D9. It means:
-
Some UI bugs belong to another repository.
-
State that both applications need is synchronised explicitly, in both directions.
-
The
MESSAGE_TYPEenum is a cross-repository contract.
Where it lives
| Concern | Location |
|---|---|
Iframe plumbing |
|
EVA integration |
|
Parent-side handling |
|
Helper |
|
Embedded surfaces
EVA_IFRAME_SELECTOR enumerates every host element, and therefore every EVA-rendered
region:
| Enum member | Host selector | Surface |
|---|---|---|
|
|
The supplier header bar. Flag |
|
|
The underwriting tracker. Flag |
|
|
Onboarding summary. Flag |
|
|
The beta underwriting view. |
|
|
Supplier contracts tab. |
|
|
Manage → contracts. |
|
|
Event rules v2 editor. |
|
|
DEV Admin flag console (embeds EVA to manage EVA’s own flags). |
|
|
DEV Admin QA surface. |
EvaService.getIframeIdBySelector(selector) resolves a selector to the iframe’s DOM id —
that is how the parent addresses a specific frame without holding a component reference.
Embedding an EVA page
<app-eva [url]="'underwriting-tracker'" [queryParams]="{ supplierKey: key }"></app-eva>
EvaComponent (shared/eva/eva.component.ts) does four things on init:
-
Reads the session directly from
localStorage['currentUser']— not from the store. -
Resolves the user’s sub-permissions via
UsersService.getPermissionSubPerms(permCode)and callssetConfig(subPermissions). -
Builds the iframe URL through
EvaService.getUrl(url, params). -
Registers its iframe id with
EvaCommunicationService.setIframeId(…).
It also clears the EVA auth cookie on window:unload and window:beforeunload, so a
refresh re-issues the token.
Authentication hand-off
EvaService has two modes, and they differ enough to matter:
getUrl(subUrl, queryParams = {}): string {
if (this.getCurrentHostname() === 'localhost') {
return `${environment.evaApiUrl}${subUrl}?auth=${this.token}&${queryParamsString}`;
}
return `${this.evaUrl}${subUrl}?${queryParamsString}`;
}
private passTokenToEva(): void {
if (this.getCurrentHostname() === 'localhost') return;
if (this.cookieService.doesCookieExist(EVA_COOKIE_TOKEN_KEY)) {
this.cookieService.delete(EVA_COOKIE_TOKEN_KEY, { path: '/', domain: environment.domain });
}
this.cookieService.setCookie(EVA_COOKIE_TOKEN_KEY, this.token,
{ path: '/', domain: environment.domain, expires: 1 });
}
| Environment | Mechanism |
|---|---|
Deployed |
A cookie named |
|
The token is appended to the iframe URL as |
|
Local and deployed behaviour differ in where the token lives. A bug that only reproduces
locally (or only when deployed) is often this. On |
The postMessage layer
CommunicationService is the transport. Key properties:
| Concern | Implementation |
|---|---|
Addressing |
Every message carries a |
Envelope |
|
Streams |
|
Origin checks |
Inbound: |
Target origin |
Outbound uses |
|
|
Debug logging |
Every send and receive is |
|
So
|
Message catalogue
shared/iframe/communication/message-type.enum.ts. Values are human-readable strings like
'[GENERAL] Config Request'.
Configuration
| Type | Direction | Meaning |
|---|---|---|
|
EVA → IA |
EVA asks for its configuration. |
|
IA → EVA |
|
|
EVA → IA |
EVA reports its own available/build flags. |
|
IA → EVA |
Clear EVA’s flag overrides. |
EvaPermissions is { permission, subPermissions: string[] } — EVA’s authorisation is
derived from what this app sends. See authorisation.
Navigation
| Type | Direction | Meaning |
|---|---|---|
|
EVA → IA |
|
|
EVA → IA |
Same handler in |
|
IA → EVA |
Built by |
|
EVA → IA |
Jump to the external sales CRM using |
Supplier state
| Type | Direction | Meaning |
|---|---|---|
|
EVA → IA |
EVA asks for supplier details. |
|
IA → EVA |
Push supplier details into EVA’s store. Sent on every change via
|
|
EVA → IA |
EVA patches supplier details in this app’s store, dispatching
|
|
IA → EVA |
Ask EVA’s header to re-fetch. |
|
both |
EVA asks; IA replies with |
|
Supplier details flow both ways: |
Actions
| Type | Direction | Meaning |
|---|---|---|
|
EVA → IA |
Trigger the login-as-supplier flow (needs the browser extension). |
|
EVA → IA |
Trigger the Amazon login flow. |
|
EVA → IA |
Start an earnings load. See lifecycle actions. |
|
EVA → IA |
Ask the parent to refresh the contracts page. |
|
EVA → IA |
|
Sizing
| Type | Effect |
|---|---|
|
Set both height and width. |
|
Set one dimension. |
|
Back to |
|
Expand and restore the supplier top bar. |
ResizeIframeService (providedIn: 'root', extends SubscriberComponent) filters
incomingMessage$ for the four resize types and applies them:
private getElement(item: ResizeItem): ResizeHTMLElement {
const result = item.differentFrameSelector
? document.querySelector(`${item.differentFrameSelector} iframe`)
: document.getElementById(item.id);
if (!result) throw new Error('Element not found!');
return result as ResizeHTMLElement;
}
differentFrameSelector lets one iframe resize another — used when a modal inside one
EVA frame needs the containing frame to grow.
|
|
The repository README documents the symmetry: a same-named ResizeIframeService exists in
both applications, and in the default direction EVA sends dimensions to Internal Admin,
which resizes the frame.
Parent-side handling
AppComponent.doActionIfMessage(message) is the single inbound switch:
GENERAL__SHOW_TOAST → toastr success | error
GENERAL__UPDATE_SUPPLIER_DETAILS_STORE → dispatch UpdateSupplierDetailsModelByEva
GET_STORE__SUPPLIER_DETAILS → reply with UPDATE_EVA_STORE__SUPPLIER_DETAILS
GENERAL__NAVIGATE_BY_LINK → router.navigate
GENERAL__ONLY_CHANGE_ROUTE → router.navigate
SUPPLIER_MENU__GET_MENU_CONDITIONS → reply with { isAmazonLending, isSellerCard }
Resize messages are handled separately by ResizeIframeService; flag messages by
FeatureFlagsEvaService.
|
|
Outbound helper
EvaCommunicationService (provided per EvaComponent, not globally) implements the
outbound command set:
interface EvaCommunicationCommands {
refetchSupplierDetails(): void;
sendUpdatedConfig(flags: string[], permissions?: EvaPermissions): void;
resetFeatureFlags(): void;
updateSupplierDetailsStore(details: SupplierDetailsModel): void;
}
updateSupplierDetailsStore resolves the top-bar frame by selector rather than using the
component’s own iframe id, and returns silently if that frame is absent.
|
|
Debugging the protocol
-
Open the console on a non-production build. Every message is logged as
[IA] Sending <type> <data> <target>or[IA] Received <type> <data>. -
If nothing is logged on receive, the message was dropped by
checkIframeIdExists,checkOrigin/checkChildOrigin, or thepluginIframeIdcomparison. -
If the frame is blank, check the token: cookie
PYBEvaTokenonenvironment.domainwhen deployed,?auth=in the iframe URL on localhost. -
If sizing is stuck, check whether a
'Element not found!'error has already killed theResizeIframeServicesubscription. -
If flags look wrong, check
sessionStorage['FeatureExecFlagsEva']and theFEATURE_FLAGS_EVAglobal setting. See feature flags.
Changing the contract
MESSAGE_TYPE is shared with the EVA repository. Adding a type is safe (the switch ignores
unknown types); changing or removing one is a coordinated release across both
applications. Add the new type, ship both sides, then remove the old one.
Gotchas summary
| Issue | Detail |
|---|---|
Parent domain token is |
|
Local vs deployed auth differs |
Query parameter vs cookie. |
Two navigation message types behave identically |
|
A missing resize target kills resizing |
The thrown error terminates the subscription. |
Menu-condition handler leaks subscriptions |
One per received message. |
|
Assigned before every send. |
Supplier details are bidirectional |
Changes must be handled on both sides. |