Hallo 😊

Kamsi Oleka, a software engineer, a gym rat, and a stand-up comedian amongst friends.

I have worked with interesting companies like Thepeer, Klarna, Piggy LLC, and Abeg.

A blog of my thoughts, experiences, and cool stuff I've built.

Back

Building an SDK part 2

Welcome back. This article is part two of this article. If you're new to this and haven't read the first part, I strongly recommend you do.

Prerequisite

  • Understanding JavaScript basics, JavaScript functions
  • Knowledge of Webpack
  • Understanding of JS package manager -- package.json file

In the previous article, I focused on how we would build the script file. We will create a simple demo app as our iframe's source. It would call the methods exposed to it by the script.

What you'll learn

  • How to build an SDK
  • Possible security vulnerabilities and how to fix them

Step 1

Open up your terminal and make a new directory called pay-app

mkdir pay-app

and create an App.jsx file

touch App.jsx

There are two ways to receive data from our script:

  • We either listen for dispatched messages with the data they're carrying or;
  • We pick the data from our search query

We will take a look at both or them.

// App.jsx
import { useEffect, useState } from "react";

const postMessageToListeners = ({ event, data }) => {
  window.parent && window.parent.postMessage({ type: event, data }, "*");
};

const App = () => {
  const [config, setConfig] = useState();

  // listening for messages starts here
  const handleMessage = (event) => {
    event.data.type === "sdkData" && !config && setConfig(event.data.config);
  };

  window.addEventListener("message", handleMessage);
  useEffect(() => {
    return () => {
      window.removeEventListener("message", handleMessage);
    };
  }, []);

  // listening for messages ends here

  // from search query starts here
  const { search } = window.location;

  useEffect(() => {
    if (!search) return;

    const parseParams = (querystring: string) => {
      const params = new URLSearchParams(querystring);
      const obj = {};
      for (const key of params.keys()) {
        obj[key] = params.get(key);
      }
      return obj;
    };

    const searchConfigObj = parseParams(search);
    setConfig(searchConfigObj);
  }, [search]);

  // from search query ends here

  if (!configObj) return <div />;
  const { publicKey, amount, meta, currency } = config;

  const handleCloseClick = () => postMessageToListeners({ event: "pay.close" });

  const handleSuccessClick = () => {
    const transactionData = {
      type: "transaction",
      transaction: {
        id: "transaction-identifier",
        remark: "medicine",
        amount: 50000,
        currency: "NGN",
        charge: 0,
        type: "peer",
        refund: false,
        channel: "send",
        status: "success",
        user: {
          name: "Tim Cook",
          identifier: "tim",
          identifier_type: "username",
          email: "tim@apple.com",
          reference: "one-more-thing",
          created_at: "2020-05-06T12:00:00.000Z",
          updated_at: "2020-05-06T12:00:00.000Z",
        },
        checkout: null,
        mode: "credit",
        reference: "transaction-reference",
        peer: {
          user: {
            name: "Kamsi Oleka",
            identifier: "ezemmuo",
            identifier_type: "username",
          },
          business: {
            name: "Apple",
            logo: null,
            logo_colour: "#ffffff",
          },
        },
        meta: {
          city: "Cupertino",
          state: "California",
        },
        created_at: "2021-04-12T19:52:22.000000Z",
        updated_at: "2021-04-12T19:52:22.000000Z",
      },
    };
    postMessageToListeners({ event: "pay.success", data: transactionData });
  };

  const handleErrorClick = () =>
    postMessageToListeners({ event: "pay.server_error" });

  return (
    <div>
      {Object.keys(config).map((key) => (
        <p key={key}>This is the {key}: config[key]</p>
      ))}

      <button onClick={handleCloseClick}>Close SDK</button>
      <button onClick={handleSuccessClick}>Simulate success</button>
      <button onClick={handleErrorClick}>Simulate error</button>
    </div>
  );
};

export default App;

The code above, although lean, covers the core functionalities our app would need.

1. Getting our data

  • by listening to dispatched messages: We set an event listener for the messages dispatched from the iframe. It picks the config object sent from the iframe and sets it to our App's state and a few functions to dispatch events for our script to handle.
  • from the search query: we pull out the query from our url and form our config object with it. Then we update the state with our config object.

    NB:

    if you pass your config data using the search query, ensure there is no sensitive data going through. The queries can be tampered with on the web.

2. handleCloseClick

A simple function to dispatch a close event to our iframe

3. handleSuccessClick

A simple function to dispatch a unique success event to our iframe with some data (ideally got from the backend)

4. handleErrorClick

A simple function to dispatch an error event to our iframe.

Step 2

Open the pay-script directory and create an pay.html file

In this pay.html file, we will run the script in the browser to test our code.

<!--pay.html-->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta
      name="viewport"
      content="width=device-width, maximum-scale=1, user-scalable=0"
    />
    <title>Pay SDK</title>
  </head>

  <body>
    <div class="p-5">
      <button id="pay">Pay now</button>
    </div>
    <script type="application/javascript">
      const launchPay = document.getElementById("pay");
      launchPay.onclick = () => {
        const config = {
          publicKey: "PUBLIC_KEY",
          userReference: "USER_REFERENCE",
          amount: 10000,
          currency: "NGN",
          onSuccess: (response) => {
            console.log("onSuccess called with:", response);
          },
          onError: (response) => {
            console.log("onError called with:", response);
          },
          onClose: (response) => {
            console.log("onClose called with:", response);
          },
        };
        const pay = new Pay(config);
        pay.setup();
        pay.open();
      };
    </script>

    <script type="application/javascript" src="./pay-script/v1/pay.js"></script>
  </body>
</html>

In the snippet above, we have two script tags in our body, one to inject our pay script into that browser instance; the other to initialize the SDK with the transaction information.

Step 3

Create a package.json file

touch package.json

and open the file and paste the following code

{
  "name": "pay.js",
  "version": "0.0.1",
  "description": "pay script",
  "main": "index.js",
  "scripts": {
    "build": "webpack --env production --mode production",
    "dev": "webpack --env development --mode development"
  },
  "publishConfig": {
    "access": "public"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kamsy/pay.js.git"
  },
  "keywords": ["pay", "sdk", "widget"],
  "author": "kamsy",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/kamsy/pay.js/issues"
  },
  "homepage": "https://github.com/kamsy/pay.js#readme"
}

Step 4

We're going to bundle our script. Install webpack and its cli; create a webpack.config.js file

npm install webpack webpack-cli
touch webpack.config.js terser-webpack-plugin

and paste the code below into the file

// webpack.config.js
const path = require("path");

module.exports = {
  entry: {
    index: [
      path.resolve(__dirname, "utils.js"),
      path.resolve(__dirname, "script.js"),
    ],
  },
  output: {
    filename: "pay-script/v1/pay.js",
    path: path.resolve(__dirname),
  },
};

The webpack config tells webpack what our entry files are and where (folder and filename) to output the built script. The following config option tells webpack how to optimize the code; we mean not to build our script with the comments in our code.

Step 5

Run the build command in your terminal and open your pay.html in the browser

npm run build

You should see the "Pay now" button when your browser opens.

Screenshot of the Pay SDK in the browser!

Click on the button to launch the SDK.

Screenshot of the Pay SDK in the browser!

The config props passed into the SDK are rendered as text, so our data came through as expected.

Now open your browser console, and click on the buttons in our demo app. You should see events and data sent over to our script.

Easy peasy!😉