Marketplace API Connectors
src/app/core/api-connectors/ monitors whether Payability can still read a
supplier’s marketplace data, and prompts a reconnect when it cannot. Two
connectors are live: Amazon and Shopify.
Orchestration
ApiConnectorsService.init() is called from
GlobalSubscriptionsService.init().
public init(): void {
if (!this.permission$) {
this.store.dispatch(fetchSupplierMarketplaces());
this.permission$ = this.store.select(currentUserPermissions).pipe(
filter(this.arePermissionsExisting),
take(1),
map(this.remapPermissions),
).subscribe((permissions: Map<number, Permission>) => {
if (permissions.get(PermissionsEnum.SHOPIFY_API_CONNECTION)) { this.shopify.init(); }
if (permissions.get(PermissionsEnum.AMAZON_API_CONNECTION)) { this.amazon.init(); }
});
}
}
Both permissions are default-granted, so both connectors normally start. The
permission$ guard makes init() idempotent.
AbstractConnectorApi
The shared base class. Subclasses supply five members:
protected abstract readonly bannerSuccessKey: string; // translation key
protected abstract readonly bannerFailKey: string; // translation key
protected abstract readonly bannerComponent: Type<AbstractConnectorBanner>;
protected abstract readonly modalComponent: Type<AbstractConnectorModal>;
protected abstract readonly storeKey: MarketplaceEnum;
protected abstract handleModalResult(result: boolean): void;
When the check runs
private isProperRouterEvent(event: RouterEvent): boolean {
return !!event
&& event instanceof NavigationEnd
&& event.urlAfterRedirects.includes('/dashboard') (1)
&& event.url
&& !event.url.includes('my-marketplaces'); (2)
}
| 1 | The check only fires on navigation to the dashboard. |
| 2 | Navigation from /my-marketplaces is excluded — the supplier is already on
the page where they would fix the connection, so interrupting them there is
counterproductive. |
init() subscribes to router events and also runs one immediate check.
What it checks
this.store$ = combineLatest([
this.store.pipe(select(loadedSupplierMarketplaceSuccess),
filter((result: any) => !result?.supplierMarketplaces?.isLoading)),
this.store.pipe(select(selectSupplierMarketplaceByKey, { key: this.storeKey }), filter(Boolean)),
this.store.pipe(select(selectCurrentUser), filter(user => this.isUserNotDuringOnboarding(user))), (1)
]).pipe(
filter(() => this.isProperRouterEvent(this.lastEvent)),
take(1),
).subscribe(([_, marketplace]) => this.handleMarketplace(marketplace as SupplierMarketplacesModel));
| 1 | Suppliers still in onboarding are never prompted — connections are expected to be incomplete at that stage. |
The reconnect decision
public shouldDisplayPopup(marketplace: SupplierMarketplacesModel): boolean {
return coerceBooleanProperty(marketplace)
&& [ApiStatus.Required.valueOf(), ApiStatus.Needed.valueOf()].includes(marketplace.apiStatus?.toLowerCase())
&& !marketplace.draft
&& !marketplace.marketplaceStatus?.hidden
&& !marketplace.marketplaceStatus?.rejected;
}
Only required and needed prompt. rejected, in-review, pending,
incomplete and confirmed do not — there is nothing the supplier can usefully
do.
protected handleMarketplace(marketplace: SupplierMarketplacesModel): void {
if (this.shouldDisplayPopup(marketplace)) {
this.showReconnectBanner();
this.verify(); // open the guided modal
} else {
this.preventCheck = true; (1)
}
}
| 1 | Once a connection is healthy, preventCheck stops all further checks for the
session — the connector does not re-poll after a clean result. |
Banners
Reconnect |
|
Connected |
|
Showing one closes the other, and both are tracked in fields so they are never duplicated.
Amazon connector
|
|
|
|
|
|
Components |
|
Amazon’s OAuth handshake happens on Amazon’s domain, so the return is detected with a cookie rather than a query parameter.
AmazonConnectorCookieService
|
Set before leaving for Amazon Seller Central; its presence on return means "we just came back". |
|
Same, for a connection started from |
Domain |
|
Max age |
24 hours |
Cookies are removed by rewriting them with max-age=-99999.
Constructor behaviour
this.isCookieAdded = this.amazonCookie.isAmazonCookieAdded;
if (this.isCookieAdded) {
this.amazonCookie.removeAmazonCookie();
}
if (this.amazonCookie.isAmazonMyMarketplaceCookieAdded) {
this.amazonCookie.removeAmazonMyMarketplaceCookie();
this.router.navigate([URLS_ROUTER.MARKETPLACE.MY_MARKETPLACES],
{ queryParams: { 'open-amazon': true } }).then(); (1)
}
| 1 | A supplier who started the connection from /my-marketplaces is returned
there with ?open-amazon=true, which reopens the Amazon panel — they land
back exactly where they were. |
Success detection
private isConnectionConfirmedAndUrlContainsMark(amazon: SupplierMarketplacesModel): boolean {
return coerceBooleanProperty(amazon) && ApiStatus.Confirmed === amazon.apiStatus && this.isCookieAdded;
}
Confirmed status plus the return cookie means the reconnect just succeeded, so the success banner is shown instead of running the normal check.
|
|
|
The cookie domain is hardcoded to |
Shopify connector
|
|
|
|
|
|
Components |
|
Shopify verifies inline instead — when the modal closes, the connector refetches the marketplace list and checks the result:
protected handleModalResult(): void {
this.store.select(selectCurrentUserId).pipe(
mergeMap(id => this.marketplacesService.getSupplierMarketplacesList(id)),
).subscribe((marketplaces: SupplierMarketplacesModel[]) => {
this.store.dispatch(loadedSupplierMarketplaceSuccess({ payload: marketplaces }));
const shopify = marketplaces?.find(m => m.marketplaceKey === MarketplaceEnum.Shopify);
if (shopify?.apiStatus === 'confirmed') {
this.preventCheck = true;
this.unsubscribe();
this.showConnectedBanner();
} else {
this.showReconnectBanner();
}
});
}
|
The comparison uses the string literal |
DI tokens
export const AMAZON_API_CONNECTOR = new InjectionToken<AmazonConnectorApiService>('amazon-api-connector');
export const SHOPIFY_API_CONNECTOR = new InjectionToken<ShopifyConnectorApiService>('shopify-api-connector');
Adding a connector
-
Create
core/api-connectors/<marketplace>/. -
Extend
AbstractConnectorApi, supplying the five abstract members andhandleModalResult(). -
Build a banner component implementing
AbstractConnectorBanner(verify()) and a modal implementingAbstractConnectorModal(connect()). -
Add
MODALS.AP_<NAME>.SUCCESSand.FAILtranslation keys. -
Add a permission to
PermissionsEnumandpermissions.const.ts, and grant it. -
Initialize the connector in
ApiConnectorsService.init()behind that permission.