v1.4.1 - July 27, 2022

Prerequisites

  1. Request application setup with your Vouchr Representative
  2. Receive a Vouchr-Hosted Web Application Url or host your own, and optionally a set of Client IDs for multi-app setups

Summary

The complete eCard integration process involves the following steps:

  1. Backend: FI to provide JWT to secure eCard experience
  2. Frontend: FI to integrate the create experience
  3. Frontend: FI to show the create widget (optional)
  4. Frontend: FI to add edit abilities (optional)
  5. Frontend: FI to add remove abilities (optional)

Backend

Authorizing Users (Backend)

A JWT, created and signed by the FI’s backend with a secured asymmetric key allows Vouchr to verify that the user is coming from a valid FI banking login. For an overview of JWTs and how they function, consult https://jwt.io/

Backend Flow Image

JWT Properties

We recommend using one of the following RSA signature methods:

PS256, PS384, PS512

We require the following claims within the JWT

  • sub - subject - should be an anonymous user-id uniquely identifies the eCard user and is constant for this customer. It must not be an id that could be used to tie the customer to information outside of the context of eCards. It should be alphanumeric, and the length must be between 14 and 64 characters. It can contain dashes. Sample value: c1ff176d-b708-4417-9968-96b0587cc439

  • exp - expiry - a timestamp which specifies the expiration of this token, we recommend 30 minutes in the future. Claim syntax is intDate. Note this must be in a numeric format, formatted as time since the Epoch in seconds. Make sure this is sent as a numeric type, without quotes. Sample Value: 1578937931

We support the following additional claims for FI’s who would like the extra validation

  • nbf - not before - a timestamp which specifies the earliest time the token may be consumed. Claim syntax is intDate. Note this must be in a numeric format, formatted as time since the Epoch in seconds. Make sure this is sent as a numeric type, without quotes. Sample Value: 1578937931.This claim is OPTIONAL.

  • iat - issued at - This claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the token. This claim is OPTIONAL. Claim syntax is intDate. Sample Value: 1578937931.

  • jti - JSON token id - The claim provides a unique identifier for the JWT. The jti claim can be used to prevent the JWT from being replayed. The jti value is case sensitive. Claim syntax is a string. This claim is OPTIONAL.

In the examples below, we will show you how to construct and sign a valid JWT.

Creating a key-pair with OpenSSL

Creating the private/public key-pair can be done with OpenSSL as shown here or with your choice of tool. This key-pair allows you to sign and Vouchr to verify the authenticity of the JWT.

Creating the private key with OpenSSL

The private key is held securely within the FI Backend, allowing it to authoritatively sign the JWT.

OpenSSL example:

openssl genrsa -out privkey.pem 2048

Creating the public key with OpenSSL

The public-key allows Vouchr Services to verify the validity of the JWT

OpenSSL example:

openssl rsa -in privkey.pem -pubout > key.pub

Share the key.pub with Vouchr to allow the verification of your JWT signatures. Store the private key securely within your backend

Selecting a JWT library.

In this guide, we have selected nimbus-jose-jwt, one of the more common JWT libraries for java. If you are using another language or would like to select a different library, we recommend consulting https://jwt.io/#libraries. As long as you can fulfill the above requirements, the JWT will work.

To include nimbuse-jose-jwt within your project, if you are using a maven-based build process, you can include it in your pom.xml with the following.

<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
 <groupId>com.nimbusds</groupId>
 <artifactId>nimbus-jose-jwt</artifactId>
 <version>8.2</version>
</dependency>

Additionally, to support RSA PSS Signatures, you’ll need the bouncycastle library if you do not have it already.

add the following to your pom.xml

<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
 <groupId>org.bouncycastle</groupId>
 <artifactId>bcprov-jdk15on</artifactId>
 <version>1.64</version>
</dependency>

Then add the bouncycastle provider at app startup with:

Provider bc = BouncyCastleProviderSingleton.getInstance();
Security.addProvider(bc);

Within the FI backend, provide a method to generate the JWT for eCards

The FI’s backend should verify the customer’s session and then generate a JWT using code similar to the following

public String getVouchrJwt(RSAKey rsaKey, String customerUuid) {
    // Create RSA-signer with the private key
    JWSSigner signer = new RSASSASigner(rsaKey);

    // Prepare JWT with claims set
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        .subject(customerUuid)
        .expirationTime(new Date(new Date().getTime() + 30 * 60 * 1000L))
        .build();

    SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.PS256).build(),claimsSet);

     // Compute the RSA signature
    signedJWT.sign(signer);
     // serialize to a compact form
    return signedJWT.serialize();
}

This signed JWT should be returned to your application via regular communication channels.

 

 

Client Integration (Frontend)

Integrating the Create Experience (Required)

Create Modal Image

Step 1: Launch the Webview/iframe

Web

We will be including the Vouchr content in an iframe. This allows you to isolate your site from Vouchr in a secure manner, while still giving you full visual control and a rich experience for your users. See the example below.

Configure an iframe then load the URL at which the Discover / Create Web Application has been deployed and pass in the following parameters

Required

Optional

  • cid the eCard Client ID (only needed in multi-app setups)
  • amt the amount of the monetary gift (eg. 50.00)
  • cur the currency of the monetary gift (eg. USD)
  • code a code the receiver should use to claim the gift (eg. XY77)
  • action Needed only if you’re using the Create Widget) to link to specific content. No value will open the default discover experience

Note that these parameters will be passed in using a Url Fragment (#) instead of Query Parameter (?) as a security measure to allow us to pass them to the iframe without transmitting over an HTTP GET request

Embed the iframe in the page (on mobile, Vouchr recommends that the iframe takes up the full width of the page):

HTML
<iframe
  id="vouchrContent"
  class="vouchrIframe"
  width="100%"
  height="100%"
  title="Vouchr Content"
  src=""
></iframe>
CSS
.vouchrIframe {
  border: 0;
}

Prepare the URL to provide as the src for the iframe using JavaScript:

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";

const jwt = "jwt"; // supplied by FI
const cid = "cid"; // Vouchr partner-supplied Client ID property
const amt = "50.0"; // the amount of the monetary gift (optional, eg. 50.00)
const cur = "USD"; // the currency of the monetary gift (optional, e.g. USD)
const action = "/"; // needed only if using the create widget to direct to specfic content

const url = `${baseUrl}/#tkn=${encodeURIComponent(
  jwt
)}&cid=${encodeURIComponent(cid)}&amt=${amt}&cur=${cur}&action=${action}`;

document.getElementById("vouchrContent").src = url;
JavaScript (ES5 syntax)
"use strict";

var baseUrl = "baseUrl";

var jwt = "jwt"; // supplied by FI
var cid = "cid"; // Vouchr partner-supplied Client ID property
var amt = "50.0"; // the amount of the monetary gift (optional, eg. 50.00)
var cur = "USD"; // the currency of the monetary gift (optional, e.g. USD)
var action = "/"; // needed only if using the create widget to direct to specfic content

var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(encodeURIComponent(action), "&amt=")
  .concat(encodeURIComponent(amt), "&currency=")
  .concat(encodeURIComponent(cur);

document.getElementById("vouchrContent").src = url;
Android
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val jwt = /* supplied by FI */
    val cid = /* Partner-supplied Client ID property */
    val amt = "50.0"; // the amount of the monetary gift (optional, eg. 50.00)
    val cur = "USD"; // the currency of the monetary gift (optional, e.g. USD)
    val action = "/"; // optional if you're using the create widget to direct to specific content

    webView.settings.javaScriptEnabled = true
    webView.settings.domStorageEnabled = true
    webView.settings.mediaPlaybackRequiresUserGesture = false
    webView.webChromeClient = WebChromeClient()
    webView.loadUrl("$baseURL/#tkn=$jwt&cid=$cid&amt=$amt&cur=$cur&action=$action")
}
iOS

When creating the WKWebView ensure that the following options are turned on. Note that if your WKWebView is initialized via a storyboard, you will have to check the “Inline Playback” box in that element’s attribute’s inspector instead of passing in a WKWebViewConfiguration programmatically.

let webConfiguration = WKWebViewConfiguration()
webConfiguration.allowsInlineMediaPlayback = true
webConfiguration.mediaTypesRequiringUserActionForPlayback = []
override func viewDidLoad() {
    super.viewDidLoad()
    let jwt = /* supplied by FI */
    let cid = /* Partner-supplied Client ID property */
    let action = /* Optional  */
    val amt = /* Optional the amount of the monetary gift (optional, eg. 50.00) */
    val cur = /* the currency of the monetary gift (optional, e.g. USD) */
    var urlString = "\(baseURL)/#tkn=\(jwt)&cid=\(cid)&amt=\(amt)&cur=\(cur)"
    if let action = action {
        urlString = urlString + "&action=\(action)"
    }
    let url = URL(string: urlString)!



    webView.load(URLRequest(url: url))
}

Troubleshooting

A. If the interface is failing to come up. Perform the below steps to troubleshoot the issue
  1. Using Chrome, visit the URL complete with base URL, client id, and token.
  2. Launch the web inspector by clicking the ‘tools’ menu, followed by ‘web developer’ and ‘inspector’.
  3. Reload the page to see relevant error details in the console, which will allow you to verify the validity of JWT and Client.

Step 2: Obtain the share URL

Web

Setup an event listener using JavaScript, to listen for the message from the iframe that contains the Vouchr URL (see the documentation on window.postMessage() for more details). Note, it is imperative that you verify the sender’s identity using the origin property. See the section on ‘Security concerns’ in the link above for more details.

JavaScript (ES6 syntax)
const receiveMessage = (event) => {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the iframe
    return;
  } else if (event.data && event.data.event && event.data.url) {
    if (event.data.event === "VOUCHER_CREATED") {
      const vouchrData = {
        url: event.data.url, // Share Url
        voucherId: event.data.voucherId,
        editAction: event.data.actions.edit,
        deleteAction: event.data.actions.delete,
        summaryAction: event.data.actions.summary,
        merchantExternalId: event.data.paymentInfo.merchant.externalId,
        updateAction: event.data.actions.update,
        shareImage: event.data.shareInfo.image,
        packagingImage: event.data.shareInfo.packagingImage,
      };

      handleVouchrData(vouchrData);
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);

const handleVouchrData = (vouchrData) => {
  // to be implemented by FI, may launch share sheet, send to internal FI systems, etc
};
JavaScript (ES5 syntax)
var receiveMessage = function receiveMessage(event) {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the domain of the discover url
    return;
  } else if (event.data && event.data.event && event.data.url) {
    if (event.data.event === "VOUCHER_CREATED") {
      var vouchrData = {
        url: event.data.url, // Share Url
        voucherId: event.data.voucherId,
        editAction: event.data.actions.edit,
        deleteAction: event.data.actions.delete,
        summaryAction: event.data.actions.summary,
        updateAction: event.data.actions.update,
        merchantExternalId: event.data.paymentInfo.merchant.externalId,
        shareImage: event.data.shareInfo.image,
        packagingImage: event.data.shareInfo.packagingImage,
      };

      handleVouchrData(vouchrData); // function implemented by the domain of the discover Url
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);

var handleVouchrData = function handleVouchrData(vouchrData) {
  // to be implemented by FI, may launch share sheet, send to internal FI systems, etc
};
Android

Attach a javascript interface to obtain the share URL

private inner class WebAppInterface {
    @JavascriptInterface
    fun postMessage(message: String) {
        val messageJson = JSONObject(message)
        if (messageJson.getString("event") == "VOUCHER_CREATED") {
            val redemptionUrl = messageJson.getString("url")
            // store the actions incase we want to remove/update the eCard later
            val deleteAction = messageJson.getObject("actions").getString("delete")
            val updateAction = messageJson.getObject("actions").getString("update")

            if (redemptionUrl.startsWith(verifiedHost)) {
                setActivityResult(redemptionUrl, deleteAction)
            } else {
                setActivityResult(null)
            }
        }
    }
}
webView.addJavascriptInterface(WebAppInterface(), "appInterface")
iOS

In the view controller that will display the WKWebView implement the WKScriptMessageHandler protocol to obtain the share URL. Then attach the WKScriptMessageHandler to the webView

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if let messageString = message.body as? String,
        let data = messageString.data(using: .utf8),
        let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {

        if eventDictionary["event"] as? String == "VOUCHER_CREATED" {
          guard let urlString = eventDictionary["url"] as? String else { return }

          guard let shareInfo = eventDictionary["shareInfo"] as? [String: Any], let
                      envelopeUrl = shareInfo["packagingImage"] as? String, let logoUrl = shareInfo["favIcon"] as? String, let messageTitle = shareInfo["title"] as? String, let messageDescription = shareInfo["description"] as? String else {
                  return
              }
          // store the values for iOS sharing
          self.redemptionUrl = urlString
          self.envelopeUrl = envelopeUrl
          self.logoUrl = logoUrl
          self.messageTitle = messageTitle
          self.messageDescription = messageDescription

          //validate url
          guard redemptionUrl.scheme == "https" && redemptionUrl.host == verifiedHost else {
              print("invalid url")
              return
          }
          // store the actions incase we want to remove/update the eCard later
          self.deleteAction = eventDictionary["actions"]["delete"]
          self.updateAction = eventDictionary["actions"]["update"]

          // handle url e.g. present the share sheet
          let items = [self]
          let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)

          // IPAD sharesheet requirement
          activityVC.excludedActivityTypes = [.airDrop]
          if let popoverController = activityVC.popoverPresentationController {
              popoverController.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2, width: 0, height: 0)
              popoverController.sourceView = self.view
              popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
          }

          present(activityVC, animated: true)
      }
    }
}
    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        return self.activityViewController(activityViewController, itemForActivityType: UIActivity.ActivityType.message) ?? ""
    }

    func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
        let buttonText = "Open"
        let footerMessage = "Brought to you by Vouchr!"

        let backgroundColor = "#0c2da1"
        let textColor = "white"
        let buttonColor = "#007aff"
        let buttonTextColor = "white"

        // Apple Mail HTML
        let htmlString = """
            <html>
              <body style="font-family: sans-serif">
                <table
                  class="main"
                  style="
                    background-color: \(backgroundColor);
                    color: \(textColor);
                    margin: auto;
                    min-width: 500px;
                    padding-top: 32px;
                    padding-bottom: 32px;
                    text-align: center;
                    width: 500px;
                  "
                >
                  <tr>
                    <td>
                      <img
                        class="logo"
                        src="\(self.logoUrl)"
                        style="height: 48px"
                      />
                      <h1>\(self.messageTitle)</h1>
                      <p>\(self.messageDescription)</p>
                      <img
                        class="envelope"
                        src="\(self.envelopeUrl)"
                        style="padding: 20px 0px 20px 0px"
                      />
                      <div>
                        <a
                          href="\(self.redemptionUrl)"
                          target="_blank"
                        >
                          <button
                            style="
                              background: \(buttonColor);
                              border-color: \(buttonColor);
                              border-radius: 999px;
                              border-style: solid;
                              color: \(buttonTextColor);
                              font-size: large;
                              height: 40px;
                              width: 200px;
                            "
                          >
                            \(buttonText)
                          </button>
                        </a>
                      </div>
                    </td>
                  </tr>
                </table>

                <table
                  class="footer"
                  style="
                    border: 1px solid #e0e0e088;
                    margin: auto;
                    min-width: 500px;
                    padding: 24px 0px 24px 0px;
                    width: 500px;
                  "
                >
                  <tr>
                    <td>
                      <p
                        class="footerMessage"
                        style="margin: auto; text-align: center; width: 70%;"
                      >
                        \(footerMessage)
                      </p>
                    </td>
                  </tr>
                </table>
              </body>
            </html>
        """

        if (activityType == .mail) {
            return htmlString
        }
        return redemptionUrl
    }
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
        // Apple Mail Subject line
        return "Check out this eCard!"
    }
webView.configuration.userContentController.add(self, name: "appInterface")

Step 3: Share the URL

The redemption URL can be inserted in your existing email/SMS notifications that are sent to the recipient, or you can allow the sender to deliver the redemption URL directly to the recipient via the native share sheet. To share the redemption URL with the native share sheet, simply adapt the following code:

Web

Unfortunately native share is poorly supported with most desktop browsers. We recommend using your own share options as a fallback. Below is an example with three options Copy to clipboard, Email, and Native share.

HTML
<div>
  <button id="email-share" onClick="email()">Email</button>
  <button id="clip-share" onClick="copy()">Copy to Clipboard</button>
  <button id="native-share" onClick="share()">Share</button>
</div>
Javascript (ES6 Syntax)
// This is the url you plan to share returned from VOUCHER_CREATED event in Step 2
const url = vouchrData.url;

const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

const copy = () => {
  copyClipboard(url);
  document.getElementById("clip-share").innerHTML = "Copied";
};
const email = () => emailShare(url);
const share = () => nativeShare(url);

if (isMobile && navigator.share) {
  // On mobile only native share is a good idea to avoid UX duplication
  document.getElementById("email-share").style.display = "none";
  document.getElementById("clip-share").style.display = "none";
} else if (!navigator.share) {
  // This is only supported on mobile and safari so we must hide
  document.getElementById("native-share").style.display = "none";
}

const copyClipboard = (string) => {
  const dummyNode = document.createElement("input");
  document.body.appendChild(dummyNode);
  dummyNode.setAttribute("id", "dummy_id");
  dummyNode.setAttribute("value", string);
  dummyNode.select();
  document.execCommand("copy");
  document.body.removeChild(dummyNode);
};

const emailShare = (string) => {
  const email = "";
  const subject = "Check out this eCard";
  const emailBody = `Hi,%0DI sent you an eCard!%0DCheck it out here: ${string}%0D%0DCheers!`;
  window.open("mailto:" + email + "?subject=" + subject + "&body=" + emailBody);
};

const nativeShare = (string) => {
  navigator
    .share({
      title: "Share eCard",
      text: string,
    })
    .then(() => console.log("Thanks for sharing!"))
    .catch((err) => console.log(`Couldn't share: `, err.message));
};
Android
ShareCompat.IntentBuilder.from(this)
	.setType("text/plain")
	.setChooserTitle("Share your eCard with...")
	.setText(redemptionUrl)
	.startChooser()
iOS
let activityVC = UIActivityViewController(activityItems: [redemptionUrl], applicationActivities: nil)
present(activityVC, animated: true)

Create Widget (Optional)

Create Widget Image

The Create Widget is the entry point from the FI’s banking app that takes the customer to the eCard flow. It improves the user experience by allowing customers to select an eCard with fewer clicks. The button also drives more usage and more repeat users since the Create Widget updates dynamically based on upcoming occasions (Christmas, Valentine’s Day, etc) and allows the FI to showcase the most popular content available.

Step 1

Configure a WebView/iframe in existing FI user interface to hold the Create Widget then load the discover URL passing in the following URL parameters:

  • tkn the JWT supplied by the backend (Alternative option see: Alternative Authentication Method)
  • cid the Vouchr Client ID
  • action=%2Fpersonalize the command to show the create widget
Web

Include the Vouchr content in an iframe. This allows you to isolate your site from Vouchr in a secure manner, while still giving you full visual control and a rich experience for your users. See the example below.

Embed the iframe in the page (on mobile, Vouchr recommends that the iframe takes up the full width of the page):

HTML
<iframe
  id="vouchrWidgetContent"
  class="vouchrIframe"
  width="100%"
  height="100%"
  title="Vouchr Content"
  src=""
></iframe>
CSS
.vouchrIframe {
  border: 0;
}

Prepare the URL to provide as the src for the iframe using JavaScript:

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";
const jwt = "jwt"; // supplied by FI
const cid = "cid"; // Vouchr partner-supplied Client ID property
const action = "/personalize"; // the command to show the create widget
const url = `${baseUrl}/#tkn=${encodeURIComponent(
  jwt
)}&cid=${encodeURIComponent(cid)}&action=${encodeURIComponent(action)}`;

document.getElementById("vouchrWidgetContent").src = url;
JavaScript (ES5 syntax)
"use strict";

var baseUrl = "baseUrl";
var jwt = "jwt"; // supplied by FI
var cid = "cid"; // Vouchr partner-supplied Client ID property
var action = "/personalize"; // the command to show the create widget
var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(encodeURIComponent(action);

document.getElementById("vouchrWidgetContent").src = url;
Android

Add a Webview to your XML. The size is up to your design, we recommend 100dp.

<WebView
  android:id="@+id/carouselView"
  android:layout_width="match_parent"
  android:layout_height="100dp"/>

Then load in the URL passing in the jwt, cid.

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	val jwt = /* supplied by FI */
	val cid = /* Partner-supplied Client ID property */
	carouselWebView.settings.javaScriptEnabled = true
	carouselWebView.webChromeClient = WebChromeClient()
	carouselWebView.loadUrl("$baseURL#tkn=$jwt&cid=$cid&action=%2Fpersonalize")
}
iOS

Add a Webview to your view controller. The size is up to your design, we recommend 100pt.

override func viewDidLoad() {
    super.viewDidLoad()
    let jwt = /* supplied by FI */
    let cid = /* Partner-supplied Client ID property */
    let url = URL(string: "\(baseURL)/#tkn=\(jwt)&cid=\(cid)&action=%2Fpersonalize")!
    webView.load(URLRequest(url: url))
}

Step 2

Web

Setup an event listener using JavaScript, to listen for the message from the iframe when the user clicks a button from the create widget . The data from this message contains a actions.personalize, that you will pass into the URL of the iframe in the next step (see the documentation on window.postMessage() for more details). Note, it is imperative that you verify the sender’s identity using the origin property. See the section on ‘Security concerns’ in the link above for more details.

JavaScript (ES6 syntax)
const receiveMessage = (event) => {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the discover/widget  url
    return;
  } else if (
    event.data &&
    event.data.event &&
    event.data.actions &&
    event.data.actions.personalize
  ) {
    if (event.data.event === "PERSONALIZE") {
      return event.data.actions.personalize;
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);
JavaScript (ES5 syntax)
var receiveMessage = function receiveMessage(event) {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the discover/widgett url
    return;
  } else if (
    event.data &&
    event.data.event &&
    event.data.actions &&
    event.data.actions.personalize
  ) {
    if (event.data.event === "PERSONALIZE") {
      return event.data.actions.personalize;
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);
Android

Attach a javascript interface to obtain the actions.personalize. This encapsulates the user’s selection and will be passed to the discover screen in the next step.

private inner class WebAppInterface {
	@JavascriptInterface
	fun postMessage(message: String) {
		val messageJson = JSONObject(message)
		if (messageJson.getString("event") == "PERSONALIZE") {
      val action = if (messageJson.has("actions")) messageJson.get("actions").getString("personalize") else null
			launchEGreeting(action)
		}
	}
}
webView.addJavascriptInterface(WebAppInterface(), "appInterface")
iOS

In the view controller that will display the WKWebView, implement the WKScriptMessageHandler protocol to obtain the actions.personalize. This encapsulates the user’s selection and will be passed to the discover screen in the next step. Then attach the WKScriptMessageHandler to the webView.

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if let messageString = message.body as? String,
        let data = messageString.data(using: .utf8),
        let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
        if eventDictionary["event"] as? String == "PERSONALIZE" {
           // get the action
           guard let action = eventDictionary["actions"]["personalize"] as? String else {
               return
           }
           let discoverViewController = //instantiate the discover view controller
           discoverViewController.action = action
           present(discoverViewController, animated: true)
       }
    }
}
carouselWebView.configuration.userContentController.add(self, name: "appInterface")

Summary Widget (Optional)

Summary Widget Image

The Summary Widget replaces the contents of the Create Widget with the selected eCard and provides quick way to view the eCard selected and provide buttons to edit or remove the eCard.

Step 1

Update the existing Create Widget webview/iframe or add a new webview/iframe to host the Summary widget. The summary widget will point to a url including the summary action returned from the create experience when an eCard was selected.

  • tkn the JWT supplied by the backend (Alternative option see: Alternative Authentication Method)
  • cid the Vouchr Client ID
  • action the data.actions.summary command returned from the discover screen during the VOUCHER_CREATED event
Web

Update the iframe used for the create widget or add a new iframe to host the Summary Widget.

Prepare the URL to provide as the src for the iframe with javascript.

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";
const jwt = "jwt"; // supplied by FI
const cid = "cid"; // Optional Vouchr partner-supplied Client ID property
const action = vouchrData.summaryAction; // the summary action returned from Create Experience Step 2
const url = `${baseUrl}/#tkn=${encodeURIComponent(
  jwt
)}&cid=${encodeURIComponent(cid)}&action=${encodeURIComponent(action)}`;

document.getElementById("vouchrWidgetContent").src = url;
JavaScript (ES5 syntax)
"use strict";

var baseUrl = "baseUrl";
var jwt = "jwt"; // supplied by FI
var cid = "cid"; // Vouchr partner-supplied Client ID property
var action = vouchrData.summaryAction; // the summary action returned from Create Experience Step 2
var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(encodeURIComponent(action);

document.getElementById("vouchrWidgetContent").src = url;
Android

Update the URL of your create widget web view using jwt, cid and action

override fun onSelectCard(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	val jwt = /* supplied by FI */
  val cid = /* Partner-supplied Client ID property, optional */
  val summaryAction = /* the summary action returned from Create Widget Step 2 */
	carouselWebView.settings.javaScriptEnabled = true
	carouselWebView.webChromeClient = WebChromeClient()
	carouselWebView.loadUrl("$baseURL#tkn=$jwt&cid=$cid&action=$summaryAction")
}
iOS

Add a Webview to your view controller. The size is up to your design, we recommend 100pt.

override func selectCard() {
    let jwt = /* supplied by FI */
    let cid = /* Partner-supplied Client ID property */
    let action = /* the summary action returned from Create Widget Step 2 */
    let url = URL(string: "\(baseURL)/#tkn=\(jwt)&cid=\(cid)&action=%2Fpersonalize")!
    webView.load(URLRequest(url: url))
}

Step 2

Web

Setup an event listener using JavaScript, or update your current event listener, to listen for the message from the iframe indicating when a user has selected ‘edit’ or ‘remove’. The data from this message contains an appropriate action , that you will pass into the main vouchr iframe to allow them to edit or remove their chosen card. Note, it is imperative that you verify the sender’s identity using the origin property. See the section on ‘Security concerns’ in the link above for more details.

JavaScript (ES6 syntax)
const receiveMessage = (event) => {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the discover iframe
    return;
  } else if (event.data && event.data.event && event.data.actions.personalize) {
    if (event.data.event === "EDIT") {
      handleEditAction(event.data.actions.edit); // See 'Allow User to Edit an eCard' section
      return;
    } else if (event.data.event === "REMOVE") {
      handleRemoveAction(); // optional, if FI wants to action on the removal themselves
      return;
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);
JavaScript (ES5 syntax)
var receiveMessage = function receiveMessage(event) {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the iframe
    return;
  } else if (event.data && event.data.event && event.data.actions.personalize) {
    if (event.data.event === "EDIT") {
      handleEditAction(event.data.actions.edit); // See 'Allow User to Edit an eCard' section
      return;
    if (event.data.event === "REMOVE") {
      handleRemoveAction(); // Optional, allows FI to add additional custom logic on removal
      return;
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);
Android

Attach a javascript interface to obtain any action chosen from the Summary Widget. This encapsulates the user’s selection and can be passed to the discover screen in the next step.

private inner class WebAppInterface {
	@JavascriptInterface
	fun postMessage(message: String) {
		val messageJson = JSONObject(message)
		if (messageJson.getString("event") == "EDIT") {
      val action = if (messageJson.has("actions") && messageJson.get("actions").has("edit")) messageJson.get("actions").getString("edit") else null
			launchEGreeting(action) // See 'Allow User to Edit an eCard'
		}
		if (messageJson.getString("event") == "REMOVE") {
			removeEGreeting() // Optional, allows FI to add additional custom logic on removal
		}
	}
}
webView.addJavascriptInterface(WebAppInterface(), "appInterface")
iOS

In the view controller that will display the WKWebView, implement the WKScriptMessageHandler protocol to obtain any actions chosen from the Summary Widget. This encapsulates the user’s selection and will be passed to the discover screen in the next step. Then attach the WKScriptMessageHandler to the webView.

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if let messageString = message.body as? String,
        let data = messageString.data(using: .utf8),
        let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
        if eventDictionary["event"] as? String == "EDIT" {
           // get the action
           guard let action = eventDictionary["actions"]["edit"] as? String else {
               return
           }
           let discoverViewController = //instantiate the discover view controller
           discoverViewController.action = action
           present(discoverViewController, animated: true)
       }
        if eventDictionary["event"] as? String == "REMOVE" {
           // Optional, allows FI to add additional custom logic on removal
       }
    }
}
carouselWebView.configuration.userContentController.add(self, name: "appInterface")

Allow a user to Edit an eCard (Optional)

You may want to allow a user to edit an existing eCard. This is achieved by passing in the edit action in the url.

In the class that will edit the eCard, instantiate a WebView, WKWebView, or iframe. Load the webview/iframe with a URL passing in the following params:

  • tkn the JWT supplied by the backend (Alternative option see: Alternative Authentication Method)
  • cid the eCard Client ID
  • editAction the edit action for the voucher that was obtained when the voucher was created
Web

Prepare the url to provide as the src for the iframe using JavaScript:

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";

const jwt = "jwt"; // supplied by FI
const cid = "cid"; // Vouchr partner-supplied Client ID property

const url = `${baseUrl}/#tkn=${encodeURIComponent(
  jwt
)}&cid=${encodeURIComponent(cid)}&action=${editAction}`;

document.getElementById("vouchrContent").src = url;
JavaScript (ES5 syntax)
"use strict";

var baseUrl = "baseUrl";

var jwt = "jwt"; // supplied by FI

var cid = "cid"; // Vouchr partner-supplied Client ID property

var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(editAction);

document.getElementById("vouchrContent").src = url;

Updating an eCard (Optional)

Sometimes when an eCard is created the amount, currency, and/or code are not yet known. In these cases you will need to update the item once you know.

In the class that will edit the eCard, instantiate a WebView, WKWebView, or iframe. The webview/iframe does not need to be added to a view but will need to remain in memory until the call to update the eCard is complete. Load the webview/iframe with a URL passing in the following params:

Required

  • tkn the JWT supplied by the backend (Alternative option see: Alternative Authentication Method)
  • updateAction the update action that was obtained when the voucher was first created

Optional

  • cid the eCard Client ID (only required in multi-app setups)
  • amt the amount of the monetary gift (optional, eg. 50.00)
  • cur the currency of the monetary gift (optional, e.g. USD)
  • code a code the receiver should use to claim the gift (eg. XY77)
Web - Updating an eCard

Prepare the url to provide as the src for the iframe using JavaScript:

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";
const jwt = "jwt"; // supplied by FI
const cid = "cid"; // Vouchr partner-supplied Client ID property
const amt = "50.0"; // the amount of the monetary gift (optional, eg. 50.00)
const cur = "USD"; // the currency of the monetary gift (optional, e.g. USD)

const url = `${baseUrl}/#$
  tkn=${encodeURIComponent(jwt)}&
  cid=${encodeURIComponent(cid)}&
  amt=${amt}&
  cur=${cur}&
  action=${updateAction}`;

document.getElementById("vouchrContent").src = url;
const receiveMessage = (event) => {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the iframe
    return;
  } else if (event.data && event.data.event) {
    if (event.data.event === "UPDATED") {
      // Success
    } else if (event.data.event === "ERROR") {
      // An Error happened, try again.
    } else {
      return;
    }
  }
};
window.addEventListener("message", receiveMessage, false);
JavaScript (ES5 syntax)
var baseUrl = "baseUrl";
var jwt = "jwt"; // supplied by FI
var cid = "cid"; // Vouchr partner-supplied Client ID property
var amt = "50.0"; // the amount of the monetary gift (optional, eg. 50.00)
var cur = "USD"; // the currency of the monetary gift (optional, e.g. USD)

var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(updateAction, "&amt=")
  .concat(encodeURIComponent(amt), "&currency=")
  .concat(encodeURIComponent(cur);

document.getElementById("vouchrContent").src = url;
var receiveMessage = function receiveMessage(event) {
  if (event.origin !== "https://partner.vc.hr") {
    // Ensure that the origin of the message is from the iframe
    return;
  } else if (event.data && event.data.event) {
    if (event.data.event === "UPDATED") {
      // Success
    } else if (event.data.event === "ERROR") {
      // An Error happened, try again.
    } else {
      return;
    }
  }
};

window.addEventListener("message", receiveMessage, false);
iOS / Android

Coming Soon

Removing an eCard (Optional)

In the class that will edit/remove the eCard, instantiate a WebView, WKWebView, or iframe. The webview/iframe does not need to be added to a view but will need to remain in memory until the call to remove an eCard is complete. Load the webview/iframe with a URL passing in the following params:

  • tkn the JWT supplied by the backend (Alternative option see: Alternative Authentication Method)
  • cid the eCard Client ID
  • deleteAction the delete action for the voucher that was obtained when the voucher was created
Web - Removing an eCard

Prepare the url to provide as the src for the iframe using JavaScript:

JavaScript (ES6 syntax)
const baseUrl = "baseUrl";

const jwt = "jwt"; // supplied by FI

const cid = "cid"; // Vouchr partner-supplied Client ID property

const url = `${baseUrl}/#tkn=${encodeURIComponent(
  jwt
)}&cid=${encodeURIComponent(cid)}&action=${deleteAction}`;

document.getElementById("vouchrContent").src = url;
JavaScript (ES5 syntax)
"use strict";

var baseUrl = "baseUrl";

var jwt = "jwt"; // supplied by FI

var cid = "cid"; // Vouchr partner-supplied Client ID property

var url = ""
  .concat(baseUrl, "/#tkn=")
  .concat(encodeURIComponent(jwt), "&cid=")
  .concat(encodeURIComponent(cid), "&action=")
  .concat(deleteAction);

document.getElementById("vouchrContent").src = url;
iOS
let url = URL(string: "\(baseURL)/#tkn=\(jwt)&cid=\(cid)&action=\(deleteAction)")!
webView.load(URLRequest(url: url))

In the class that has the WKWebView, implement the WKScriptMessageHandler protocol to confirm that the delete succeeded.

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if let messageString = message.body as? String,
        let data = messageString.data(using: .utf8),
        let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
        if eventDictionary["event"] as? String == "REMOVE" {
            // delete succeeded.
            let deletedVoucherId = eventDictionary["voucherId"] as? String
        }
    }
}
Android
webView.settings.javaScriptEnabled = true
webView.webChromeClient = WebChromeClient()
webView.loadUrl("$baseURL/#tkn=$jwt&cid=$cid&action=$deleteAction")

Attach a javascript interface to confirm the delete succeeded.

private inner class WebAppInterface {
    @JavascriptInterface
    fun postMessage(message: String) {
        val messageJson = JSONObject(message)
        if (messageJson.getString("event") == "REMOVE") {
            // Delete succeeded
        } else if (messageJson.getString("event") == "ERROR") {
            // Delete error
            if (messageJson.getString("status") == null) {
                // No http status means a timeout error
                // If this occurs we must retry the delete
            }
        }
    }
}
webView.addJavascriptInterface(WebAppInterface(), "appInterface")

Alternative Authentication Method

Instead of passing in JWT using the tkn parameter you can provide the JWT via postMessage to the iframe/webview.

If using this method, we recommend you delay loading until the authentication has been received by passing &waitForAuth=true as a url parameter. This will block any ui interactions until the authentication message has been received. eCards can not be created until authentication has succeeded.

HTML
<iframe
  id="vouchrContent"
  class="vouchrIframe"
  width="100%"
  height="100%"
  title="Vouchr Content"
  src=""
></iframe>
JavaScript
window.document.addEventListener('readystatechange', () => {
    const baseUrl = "baseUrl";
    const jwt = "jwt"; //supplied by FI
    if (window.document.readyState == 'complete') {
        document.getElementById('vouchrContent').contentWindow.postMessage({type: 'auth', token: jwt}, baseUrl);
    }
});

document.getElementById("vouchrContent").src = url + "&waitForAuth=true";

Android

Create a custom WebViewClient and set the web view client before loading the url

webView.setWebViewClient(new WebViewClient());
webView.loadUrl(url + "&waitForAuth=true");
private static class MyWebViewClient extends WebViewClient {
  boolean loginInitialized = false;
  String baseUrl = "baseUrl"
  String jwt = "jwt" //supplied by FI
  @Override
  public void onPageFinished(WebView view, String url) {
      super.onPageFinished(view, url);
      String script = String.format("postMessage({type: 'auth', token: '%s'}, '%s');", jwt, baseUrl);
      if (!loginInitialized) {
          loginInitialized = true;
          view.evaluateJavascript(script, new ValueCallback<String>() {
              @Override
              public void onReceiveValue(String value) {
              }
          });
      }
  }
}
IOS
override func viewDidLoad() {
  super.viewDidLoad()
  let baseUrl = /* supplied by FI */
  let jwt = /* supplied by FI */
  let cid = /* Partner-supplied Client ID property */
  let action = /* Optional  */
  val amt = /* Optional the amount of the monetary gift (optional, eg. 50.00) */
  val cur = /* the currency of the monetary gift (optional, e.g. USD) */
  var urlString = "\(baseURL)/#cid=\(cid)&amt=\(amt)&cur=\(cur)&waitForAuth=true"
  if let action = action {
      urlString = urlString + "&action=\(action)"
  }
  let url = URL(string: urlString)!

  let javascript = "postMessage({type: 'auth', token: '\(jwt)'}, '\(baseUrl)');"
  let userScript = WKUserScript(source: javascript, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
  
  self.webview.configuration.userContentController.addUserScript(userScript)

  webView.load(URLRequest(url: url))
}

Full Examples

Below is a full example of launching the eCard experience.

Android

Add the following layout and activity that will handle the eCard experience and return the redemption to the calling activity.

activity_egreeting.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".EGreetingActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <WebView
        android:id="@+id/webView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

</androidx.constraintlayout.widget.ConstraintLayout>

EGreetingActivity.kt

private const val verifiedHost = /* supplied by FI */
private const val baseURL = /* supplied by FI */
private const val cid = /* Partner-supplied Client ID property */

class EGreetingActivity : AppCompatActivity() {

    private inner class WebAppInterface {
        @JavascriptInterface
        fun postMessage(message: String) {
            val messageJson = JSONObject(message)
            if (messageJson.getString("event") == "VOUCHER_CREATED") {
                val redemptionUrl = messageJson.getString("url")
                if (redemptionUrl.startsWith(verifiedHost)) {
                    setActivityResult(redemptionUrl)
                } else {
                    setActivityResult(null)
                }
            }
        }
    }

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_egreeting)
        setSupportActionBar(toolbar)

        webView.settings.javaScriptEnabled = true
        webView.settings.mediaPlaybackRequiresUserGesture = false
        webView.webChromeClient = WebChromeClient()
        webView.addJavascriptInterface(WebAppInterface(), "appInterface")

		val token = intent.getStringExtra(TOKEN_PARAM)
		val pParam = intent.getStringExtra(PERSONALIZE_PARAM)

		val url = "$baseURL#tkn=$token&cid=$cid" + (if (pParam != null) "&$pParam" else "")
		webView.loadUrl(url)
    }


    /**
     * Sets the result that the activity will return to its caller.
     **/
    private fun setActivityResult(shareUrl: String?) {
        val intent = Intent()
        intent.putExtra(URL_PARAM, shareUrl)
        setResult(Activity.RESULT_OK, intent)
        finish()
    }

    /**
     * Called when the activity has detected the user's press of the back key.
     */
    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
            return
        }
        super.onBackPressed()
    }

    companion object {
        const val URL_PARAM = "URL"
        private const val TOKEN_PARAM = "TOKEN"
        private const val PERSONALIZE_PARAM = "PERSONALIZE"

        fun startActivityForResult(activity: Activity, resultCode: Int, jwtToken: String, personalizeParam: String? = null) {
            val intent = Intent(activity, EGreetingActivity::class.java)
            intent.putExtra(TOKEN_PARAM, jwtToken)
            intent.putExtra(PERSONALIZE_PARAM, personalizeParam)
            activity.startActivityForResult(intent, resultCode)
        }
    }
}

Then you can launch the activity and override onActivityResult to obtain the redemptionURL.

private const val RESULT_CODE = 0

class MainActivity : AppCompatActivity() {

	private const val baseURL = /* supplied by FI */

    // You only need this if you ARE using the Create Widget
	private inner class WebAppInterface {
		@JavascriptInterface
		fun postMessage(message: String) {
			val messageJson = JSONObject(message)
			if (messageJson.getString("event") == "PERSONALIZE") {
				val personalizeAction = messageJson.get("actions").getString("personalize")
				launchEGreeting(personalizeAction)
			}
		 }
	}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

		// If you're NOT using the Create Widget
        launchBtn.setOnClickListener {
            launchEGreeting()
        }
        // If you ARE using the Create Widget
        val jwt = /* supplied by FI */
	    val cid = /* Partner-supplied Client ID property */
        personalizationView.settings.javaScriptEnabled = true
		personalizationView.webChromeClient = WebChromeClient()
		personalizationView.addJavascriptInterface(WebAppInterface(), "appInterface")
		personalizationView.loadUrl("$baseURL#tkn=$jwt&cid=$cid&action=%3D%2Fpersonalize")
    }

	private fun launchEGreeting(action: String? = null) {
		    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        launchBtn.setOnClickListener {
            EGreetingActivity.startActivityForResult(this, RESULT_CODE, jwt, action)
	)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == RESULT_CODE && resultCode == Activity.RESULT_OK) {
            val redemptionUrl = data.getStringExtra(EGreetingActivity.URL_PARAM)
        }
    }

}

iOS

Create a view controller with a WKWebview and set its class to EGreetingButtonViewController. Assign the webView’s Outlet to the view controller’s WKWebview.

EGreetingButtonViewController.swift

import UIKit
import WebKit

class EGreetingButtonViewController: UIViewController, WKScriptMessageHandler {
    @IBOutlet weak var webView: WKWebView!

    let jwt = /* supplied by FI */
    let cid = /* Partner-supplied Client ID property */
    let baseUrl = /* supplied by FI */

    override func viewDidLoad() {
        super.viewDidLoad()
        let url = URL(string: "\(baseURL)/#tkn=\(jwt)&cid=\(cid)&action=%2Fpersonalize")!
        webView.configuration.userContentController.add(self, name: "appInterface")
        webView.load(URLRequest(url: url))
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let messageString = message.body as? String,
            let data = messageString.data(using: .utf8),
            let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {

            if eventDictionary["event"] as? String == "PERSONALIZE" {
                // get the action
                guard let action = eventDictionary["actions"]["personalize"] as? String else {
                    return
                }
                let eCardViewController = // instantiate view controller
                eCardViewController.action = action
                present(activityVC, animated: true)
            }
        }
    }
}

Create a view controller with a WKWebview and set its class to EGreetingViewController. Assign the webView’s Outlet to the view controller’s WKWebview.

EGreetingViewController.swift

import UIKit
import MessageUI
import WebKit

class EGreetingViewController: UIViewController, UIActivityItemSource, WKScriptMessageHandler {
    @IBOutlet weak var webView: WKWebView!

    let verifiedHost: String = ""// supplied by the FI
    let jwt = /* supplied by FI */
    let cid = /* Partner-supplied Client ID property */
    let baseUrl = /* supplied by FI */
    var action: String?

    var redemptionUrl = "" /* initialize */
    var envelopeUrl = "" /* initialize */
    var logoUrl = "" /* initialize */
    var messageTitle = "" /* initialize */
    var messageDescription = "" /* initialize */

    override func viewDidLoad() {
        super.viewDidLoad()
        var urlString = "\(baseURL)/#tkn=\(jwt)&cid=\(cid)"
        if let action = action {
            urlString = urlString + "&action=\(action)"
        }
        let url = URL(string: urlString)!
        webView.configuration.userContentController.add(self, name: "appInterface")
        webView.load(URLRequest(url: url))
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let messageString = message.body as? String,
            let data = messageString.data(using: .utf8),
            let eventDictionary = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {

            if eventDictionary["event"] as? String == "VOUCHER_CREATED" {
                guard let urlString = eventDictionary["url"] as? String else { return }

                guard let shareInfo = eventDictionary["shareInfo"] as? [String: Any], let
                            envelopeUrl = shareInfo["packagingImage"] as? String, let logoUrl = shareInfo["favIcon"] as? String, let messageTitle = shareInfo["title"] as? String, let messageDescription = shareInfo["description"] as? String else {
                        return
                    }
                // store the values for iOS sharing
                self.redemptionUrl = urlString
                self.envelopeUrl = envelopeUrl
                self.logoUrl = logoUrl
                self.messageTitle = messageTitle
                self.messageDescription = messageDescription

                // validate url
                guard redemptionUrl.scheme == "https" && redemptionUrl.host == verifiedHost else {
                    print("invalid url")
                    return
                }
                // handle url e.g. present the share sheet
                let items = [self]
                let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)

                // IPAD sharesheet requirement
                activityVC.excludedActivityTypes = [.airDrop]
                if let popoverController = activityVC.popoverPresentationController {
                    popoverController.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2, width: 0, height: 0)
                    popoverController.sourceView = self.view
                    popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
                }

                present(activityVC, animated: true)
            }
        }
    }

    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        return self.activityViewController(activityViewController, itemForActivityType: UIActivity.ActivityType.message) ?? ""
    }

    func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
        let buttonText = "Open"
        let footerMessage = "Brought to you by Vouchr!"

        let backgroundColor = "#0c2da1"
        let textColor = "white"
        let buttonColor = "#007aff"
        let buttonTextColor = "white"

        // Apple Mail HTML
        let htmlString = """
            <html>
              <body style="font-family: sans-serif">
                <table
                  class="main"
                  style="
                    background-color: \(backgroundColor);
                    color: \(textColor);
                    margin: auto;
                    min-width: 500px;
                    padding-top: 32px;
                    padding-bottom: 32px;
                    text-align: center;
                    width: 500px;
                  "
                >
                  <tr>
                    <td>
                      <img
                        class="logo"
                        src="\(self.logoUrl)"
                        style="height: 48px"
                      />
                      <h1>\(self.messageTitle)</h1>
                      <p>\(self.messageDescription)</p>
                      <img
                        class="envelope"
                        src="\(self.envelopeUrl)"
                        style="padding: 20px 0px 20px 0px"
                      />
                      <div>
                        <a
                          href="\(self.redemptionUrl)"
                          target="_blank"
                        >
                          <button
                            style="
                              background: \(buttonColor);
                              border-color: \(buttonColor);
                              border-radius: 999px;
                              border-style: solid;
                              color: \(buttonTextColor);
                              font-size: large;
                              height: 40px;
                              width: 200px;
                            "
                          >
                            \(buttonText)
                          </button>
                        </a>
                      </div>
                    </td>
                  </tr>
                </table>

                <table
                  class="footer"
                  style="
                    border: 1px solid #e0e0e088;
                    margin: auto;
                    min-width: 500px;
                    padding: 24px 0px 24px 0px;
                    width: 500px;
                  "
                >
                  <tr>
                    <td>
                      <p
                        class="footerMessage"
                        style="margin: auto; text-align: center; width: 70%;"
                      >
                        \(footerMessage)
                      </p>
                    </td>
                  </tr>
                </table>
              </body>
            </html>
        """

        if (activityType == .mail) {
            return htmlString
        }
        return redemptionUrl
    }

    func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
        // Apple Mail Subject line
        return "Check out this eCard!"
    }

    /*
     * Connect this IBAction method to a separate back button if desired (optional)
     */
    @IBAction func browserBack() {
        if (self.webview.canGoBack){
            self.webview.goBack()
        }
    }
}

ChangeLog

v1.4.1 - July 27, 2022

  • Add instruction on how to send auth token from main frame via post message

v1.4.0 - May 6, 2022

  • Add instructions for using Summary widget
  • Updated terminology

v1.3.7 - December 3, 2021

  • Add iOS method that can connect to a button for browser back feature

v1.3.6 - October 18, 2021

  • Add iOS Apple mail split share option, update web event to VOUCHER_CREATED, add merchant externalId

v1.3.5 - September 2, 2021

  • Make it clear that Client ID is optional in prerequisites

v1.3.4 - January 12, 2020

  • Added section about Javascript Sharing

v1.3.3 - December 1, 2020

  • Updated documentation with prerequisites for docs.vouchrsdk.com site

v1.3.2 - October 8, 2020

  • Added inline playback information for displaying our discover in Webviews on both android and iOS.

v1.3.1 - October 8, 2020

  • added amt and currency optional parameters to urls

v1.2.1 - March 17, 2020

  • updated share section to make it clear that share sheet or custom sharing methods are both possible

v1.2.0 - February 12, 2020

  • added Web documentation

v1.1.1 - January 16, 2020

  • added “Removing an eCard” section

v1.1.0 - January 16, 2020

  • added Create Widget instructions
  • added troubleshooting section

v1.01 - November 18, 2019

  • added Client ID (cid) parameters to urls