Implementing Wildlink Coupons into an existing Chrome Extension

The @wildlink/wildlink-coupons module is a library designed to integrate your current existing extension with the ability to try and automatically apply coupons in real time.

The @wildlink/wildlink-coupons module requires a few dependencies in order to work properly, and one of them being the wildlink-js-client. You can view details about the wildlink-js-client package functionality here: https://www.npmjs.com/package/wildlink-js-client

How it works

The @wildlink/wildlink-coupons module uses elements on the page to identify if the user is on a checkout page. When the user is on the checkout page, we dispatch events to allow the application to show a UI to the user for when they want to apply the coupons. Once the start method in the foreground api is called, the coupons are applied one by one automatically. The api will complete its process when one of the cases below are met:

  • There are no more coupons to apply.
  • The user has navigated away from the page.
  • We have reached the maxRuntime.

Once we have applied all of the coupons or if we have reached the runtime limit, the module will attempt to find the coupon that saves the most money, and apply it automatically.

Requirements

Packages:

Permissions:

  • "storage"
  • "unlimitedStorage"
  • "tabs"
  • "background"

💡 The unlimitedStorage permission is more of a recommendation than a requirement. You will receive a warning in your background console when running without it, the lack of this permission may result in unexpected behavior.

Content Scripts:

Must run at document_start

💡 Having your content scripts run at document_start implies that the content script running the @wildlink/wildlink-coupons library in the foreground will need to run at document_start. The reason for this is that you will need to initialize the @wildlink/wildlink-coupons library in the foreground by calling its init method along with adding your own event listeners when doing the integration. This process has a small start up time which will result in a race condition if done after the document has finished loading. Resulting in unexpected behavior when receiving the events from the background process.

Manifest Versions Supported:

  • V2
  • V3

Installation

The @wildlink/wildlink-coupons library is a private npm package. Please communicate with the Wildfire team to get access and please avoid exposing the package on Github or your source control management tool. You will also need to install the wildlink-js-client package if you haven’t already.

npm install @wildlink/wildlink-coupons wildlink-js-client

Integration

This section is split into two parts. The background process is the JavaScript file that runs in the background process commonly referred to as background.js, if you are using manifest version v3 you may refer to the background process as the service worker .

Background

import WildlinkCoupons from '@wildlink/wildlink-coupons/background';
import { WildlinkClient } from 'wildlink-js-client';

const initializeBackground = async () => {
const client = new WildlinkClient('SECRET', 0); // <- Your application secret and application ID from Wildfire
await client.init();
const trackingCode = 'abc123'; // <- The tracking code you use to identify your user to accredit cashback if you choose to do so, this is optional
const wildlinkCoupons = new WildlinkCoupons({ client, trackingCode });
await wildlinkCoupons.init();
wildlinkCoupons.start();
};

constructor({client: WildlinkClient, trackingCode?: string, partnerApi?: NSPartnerApi.IPartnerApi, debugMode?: boolean})

  • client: Before passing in the wildlink client it must be initialized or this method will throw an error.
  • trackingCode: The tracking code is used to track your user’s purchases. This can be used to accredit your users with cash back if you choose to do so. The tracking code is optional, and can be added later using the setTrackingCode method. The tracking code is commonly a user’s id.
  • partnerApi: This is an optional api you may provide to inject your own coupons and targets into the module. This will be explained further below.
  • debugMode: this flag is false by default, but is used to display logs in the console when testing or debugging the application.

init(): Promise<void>

Must be called when initializing the background process. Internally it initializes all of the necessary event listeners to communicate with the foreground library. This step also validates your permissions set and will throw an error if the required permissions are missing.

start(): void

This method will be used when you want to explicitly start the library. This will allow communication with the foreground process. You can call this method later if you’d like. For example only after the user has logged in, or accepted some terms and conditions. By default, the library starts in a stopped state.

stop(): void

This method will be used when you want to explicitly stop the library. This will stop all communication with the foreground process. This method is useful for stopping the extension when the user has logged out, or ran out of some subscription.

Foreground

import WildlinkCoupons from '@wildlink/wildlink-coupons/foreground';

const wildlinkCoupons = new WildlinkCoupons();

const initializeForeground = async () => {
await wildlinkCoupons.init();

injectReactComponent(<App wildlinkCoupons={wildlinkCoupons} />);
};

💡 This small code snippet uses React as an example but was designed to work in any framework you choose to use, Including jQuery or Vanilla JS.

Foreground API Explained

constructor(debugMode?: boolean)

  • debugMode: this flag is false by default, but is used to display logs in the console when testing or debugging the application.

init(): Promise<void>

Must be called when initializing the foreground process. Internally it awakens the service worker if you are using manifest v3. This step also adds listeners to communicate with the background process.

cancel(): Promise<void>

Must be called when the user dismisses your popup from your extension. This frees up resources and prevents the foreground process from affecting your user’s experience.

maxRuntime: number

This property gets and sets the maximum amount of time the module will apply coupons before completing the process. By default this value is 60. Once the coupons have been applying for more than 60 seconds, the module will automatically apply the code that saved the most money.

Integrating your foreground process behavior with the library’s

The wildlink coupons library in the foreground process extends from EventTarget supplied by the browser with some custom behavior. We use custom events for your application to listen to, along with events to help update your UI and end the process as well.

Foreground Events

Name: onReady

Fires off when we are ready to apply coupons

Event Interface:

{
merchantName: string; <-- The merchants name
couponsFound: number; <-- Show the user how much coupons are ready to try
onStart: () => Promise<void>; <-- Call when the user accepts the button to apply coupons
}

Example:

wildlinkCoupons.addEventListener('onReady', e => {
const eventData = e.detail;
console.log(eventData.couponsFound);
eventData.onStart();
});

Name: onNextCoupon

Fires off when we apply the next coupon, updates the progress in however your ui displays it

Event Interface:

{
progressPercentage: number; <-- number from 0 - 100 to show the user progress
}

Example:

wildlinkCoupons.addEventListener('onNextCoupon', e => {
const eventData = e.detail;
console.log(eventData.progressPercentage);
});

Name: onComplete

Fires off when the coupons have finished applying

Event Interface:

{
wasSuccessful: boolean; <-- Boolean that shows whether the user has saved money or not
amountSaved: number; <-- Floating point number showing how much user has saved in USD
workingCouponCode: string; <-- The coupon code that worked and saved them money
}

Example:

wildlinkCoupons.addEventListener('onComplete', e => {
const eventData = e.detail;
console.log(eventData.wasSuccessful);
console.log(eventData.amountSaved);
console.log(eventData.workingCouponCode);
});

Name: onContinue

Fires off when the page has refreshed due to a coupon being applied, if we are still in a coupon session this will fire off and at this time you want to display your ui again

Event Interface:

{
merchantName: string; <-- Merchants name
currentProgressPercentage: number; <-- Percentage of current progress
}

Example:

wildlinkCoupons.addEventListener('onContinue', e => {
const eventData = e.detail;
console.log(eventData.wasSuccessful);
console.log(eventData.amountSaved);
console.log(eventData.workingCouponCode);
});

Injecting your own Coupons and Targets into the module

The module supports the ability for you to supply your own coupon codes and targets to merge with our own api's data. Providing a unified approach for you to extend the usability of the module.

How it works

When instantiating the module you will provide your own api matching the following interface:

interface IPartnerApi {
isSupportedDomain(domains: string[]): Promise<boolean>;
getSupportedDomain(domains: string[]): Promise<PartnerDomain | null>;
fetchCouponData(domain: string): Promise<Coupons.ResponseData | null>;
}

Here is an example implementation of what your api may look like:

// The implements keyword here is optional and only works in typescript.
// This just enforces the proper types in your class
export class MyPartnerApi implements NSPartnerApi.IPartnerApi {
async isSupportedDomain(domains: string[]): Promise<boolean> {
// handle your logic to see if our list of domains are found in your data store
// return true if any of the domains in the list are supported, or false if not
return true;
}
async getSupportedDomain(domains: string[]): Promise<NSPartnerApi.PartnerDomain | null> {
// handle your logic to fetch a domain object containing information about the merchant
return {
ActivationURL: '<https://example.activationurl.com/123>',
Domain: 'merchant.com',
Merchant: {
Name: 'My Merchant Name',
},
};
// Or return null if you were unable to find a supported domain matching any domains in the list
return null;
}
async fetchCouponData(domain: string): Promise<Coupons.ResponseData | null> {
// handle your api call for your coupon data
return {
Codes: [
{
ID: 0,
Code: 'DISCOUNT123',
},
],
Targets: [
{
Before: '#before',
Error: '.error div',
ID: 0,
Input: '#coupon-code-input',
Price: '#total-price',
Remove: 'button.remove',
Submit: 'button.submit',
Timeout: 0,
},
],
};

// or return null if not found
return null;
}
}

Method Implementation Details

The example calls are here to only show you examples of how we will be using your api internally. You will not be calling these methods yourself.

isSupportedDomain(domains: string[]): Promise<boolean>

This method will be called on every web page the user visits. In theory you would have a list of domains stored somewhere in the user's browser, and when this method is called, you will internally check that list to see if any of the domains in the argument exist in your data store.

Example call:

if (await myPartnerApi.isSupportedDomain(['shop.macys.com', 'macys.com'])) {
// internal logic ...
}

getSupportedDomain(domains: string[]): Promise<NSPartnerApi.PartnerDomain | null>

This method will not be called until isSupportedDomain is called. This gives you the freedom to make an api call without worrying about an api call being made on every single web page visit. In theory this method will retrieve the domain information you will host on a server, or even on the user's device.

Example call:

const hostnames = ['shop.macys.com', 'macys.com'];
if (await myPartnerApi.isSupportedDomain(hostnames)) {
const domain = await myPartnerApi.getSupportedDomain(hostnames);
// We use your domain to fetch our internal data or your internal data
}

fetchCouponData(domain: string): Promise<Coupons.ResponseData | null>;

This method will be called to fetch the coupon and target data from your server. We will not call this method until we have identified that your api will support this domain. This keeps us from making an api call on every web page visit.

Example call:

const hostnames = ['shop.macys.com', 'macys.com'];
if (await myPartnerApi.isSupportedDomain(hostnames)) {
const domain = await myPartnerApi.getSupportedDomain(hostnames);
const couponData = await myPartnerApi.fetchCouponData(domain.Domain);
// Use your coupon data along side ours in the coupon module.
}

Interface Details

NSPartnerApi.PartnerDomain

interface PartnerDomain {
Domain: string;
ActivationURL: string | null;
Merchant?: {
Name: string;
};
}

This interface is expected to be returned by getSupportedDomain. This will be the data representing a domain you support with your own coupons and targets.

Domain: string

This property in theory will be used to match the hostname.

Example: 'macys.com'

ActivationURL: string | null

This property will contain a url for you to affiliate yourself in the background if our own internal api does not support this merchant. If you do not have a url to affiliate yourself for this merchant, you may leave this as null.

Example: '<https://example.activationurl.com/123'>

Merchant?: { Name: string; }

This property is optional. The merchant name will be used when dispatching the onReady event. If a merchant name is not provided, that event will dispatch a merchant name with an empty string.

Example:

      Merchant: {
Name: 'My Merchant Name',
},

Coupons.ResponseData

  interface ResponseData {
Targets: Target[];
Codes: Code[];
}

This interface is expected to be returned by fetchCouponData. It represents your coupon codes and targets and will be explained in further detail below.

Targets: Target[]

  interface Target {
readonly ID: number;
readonly Input: string;
readonly Submit: string | null;
readonly Remove: string | null;
readonly Price: string;
readonly Error: string | null;
readonly Before: string | null;
readonly Timeout: number | null;
}

ID: number

This property is used internally to identify targets by an ID. Any targets your api will provide will be sanitized with IDs set to 0. This prevents any confusion or errors when we store this information in our api for analytics purposes.

Example: 0

Input: string;

This property is a string representing a CSS selector for the promo code field, where you type the coupon codes in.

Example: '#coupon-code-input'

Submit: string | null;

This property is a string representing a CSS selector for the element that you click to submit the coupon into the web page. If your web page does not have a submit button, you may leave this as null. If null is provided, the algorithm will attempt to either dispatch a submit event, or simulate and enter button press on the input field.

Example: 'button.submit'

Remove: string | null;

This property is a string representing a CSS selector for the element that you click to remove the coupon code before applying another. This property is required if you want the algorithm to apply the coupon that saves the most money. If this is not provided, the algorithm will apply the first code that will work.

Example: 'button.remove'

Price: string;

This property is a string representing a CSS selector for the element containing the total price, this is required for the algorithm to work properly.

Example: '#total-price'

Error: string | null;

This property is a string representing a CSS selector for the element that displays an error when something has gone wrong. This is completely optional and only set aside for future use.

Example: '.error div'

Before: string | null;

This property is a string representing a CSS selector for the element you click to show the input field when applying a coupon. This is optional because not all web pages require you to click something to show the input field.

Example: '#before'

Timeout: number

This property is a number representing how many milliseconds to wait before we either submit the coupon or apply another, by default this is zero, if this value is zero we have our own default values we use in the extension.

Example: 1500

Codes: Code[]

  interface Code {
readonly ID: number;
readonly Code: string;
}

ID: number

This property is used internally to identify coupons by an ID. Any coupons your api will provide will be sanitized with IDs set to 0. This prevents any confusion or errors when we store this information in our api for analytics purposes.

Code: string

This property is a string representing the coupon code. The module will automatically exclude duplicates in your array, and will also remove any codes that are falsy (empty string, null, etc.).