Integrate Frames

How it works

To integrate Frames into your site there are a few steps that you need to follow:

  1. Initialise the frames SDK and host the frames:

    1. Define a placeholder element where the capture elements will be inserted into the page. This element needs to have an id defined.
    2. Start a new card capture action, the action will handle all interactions with your elements such as; creation, validation and submission. This call will need to be repeated between subsequent card captures.
    3. Create the Frames capture element by calling the createFramesControl method on the action and passing in the element type and the id of the DOM element that you would like to attach it to.
  2. Validate the captured data using events:

    1. Once the user has entered their credit card details you are going to want to validate that all the captured data is valid. To do so use the raised error events.
    2. You are also going to want to validate that all the required information has been captured. To do so use the onBlur and onFocus events.
  3. Tokenize the captured card:

    1. Once you have confirmed the data is valid you will want to submit the captured information and tokenize the card. To do this add a Submit button to the page calling the submit function on the action. This will run the card validation and submit the form if successful.
    2. Once a card capture action has been successfully submitted you'll need to complete the action by calling the complete method on the action.

Initialising the Frames

Instantiate the Frames

import * as frames from '@wpay/frames';

const environment = 'pt-api';
const baseUrl = `https://${environment}.wpay.com.au/wow/v1`;

const apiKey = 'YOUR-API-KEY';
const framesApiBaseUrl = `${baseUrl}/pay/instore`;
const walletApiBaseUrl = `${baseUrl}/pay`;

//Instantiate the frames SDK, this will allow us to capture user card information.
const framesSDK = new frames.FramesSDK({
    apiKey: apiKey,
    authToken: `Bearer ${authorizationToken}`,
    apiBase: framesApiBaseUrl,
    logLevel: frames.LogLevel.DEBUG
});

// Once the page has loaded, initialise a new card capture action.
action = framesSDK.createAction(frames.ActionTypes.CaptureCard);
await action.start();
import androidx.lifecycle.MutableLiveData

import au.com.woolworths.village.sdk.Wallet

import au.com.wpay.frames.types.FramesConfig
import au.com.wpay.frames.types.LogLevel
import au.com.wpay.frames.types.ThreeDSEnv
import au.com.wpay.frames.types.ActionType

import au.com.wpay.frames.JavascriptCommand
import au.com.wpay.frames.BuildFramesCommand
import au.com.wpay.frames.StartActionCommand

data class CardCaptureOptions(
    val wallet: Wallet?,
    val require3DS: Boolean
)

val environment = "pt-api"
val yourAPIkey = "YOUR-API-KEY";
val accessToken = "ACCESS-TOKEN";
val baseUrl = "https://${environment}.wpay.com.au/wow/v1"
val framesApiBaseUrl = `${baseUrl}/pay/instore`

val framesConfig = FramesConfig(
    apiKey = yourAPIkey,
    authToken = "Bearer $accessToken",
    apiBase = framesApiBaseUrl,
    logLevel = LogLevel.DEBUG
)

/*
* The command that needs to be executed in the Frames SDK
*/
val framesCommand: MutableLiveData<JavascriptCommand?> = MutableLiveData(null)

const val CAPTURE_CARD_ACTION = "cardCapture"

var customerWallet: Wallet? = null

fun cardCaptureCommand(payload: ActionType.CaptureCard.Payload): JavascriptCommand =
    BuildFramesCommand(
        ActionType.CaptureCard(payload).toCommand(CAPTURE_CARD_ACTION),
        StartActionCommand(CAPTURE_CARD_ACTION)
    )

fun cardCaptureOptions(options: CardCaptureOptions) =
    ActionType.CaptureCard.Payload(
        verify = true,
        save = true,
        env3DS = when(options.require3DS) {
            true -> ThreeDSEnv.STAGING
            else -> null
        }
    )

// onPageLoaded
val cardCaptureOptions = cardCaptureOptions(CardCaptureOptions(
    wallet = Wallet.MERCHANT,
    require3DS = false
))

framesCommand.postValue(cardCaptureCommand(cardCaptureOptions))
import WPayFramesSDK

public enum Wallet {
	/** Indicates the wallet has been registered with a merchant */
	case MERCHANT
}

public class FramesView : WKWebView, WKScriptMessageHandler {
	private var sdkConfig: FramesConfig?

    let environment = "substitute environment-value here"
    let yourAPIkey = "YOUR-API-KEY";
    let accessToken = "ACCESS-TOKEN";
    let baseUrl = "https://\(environment).mobile-api.woolworths.com.au/wow/v1"
    let framesApiBaseUrl = "\(baseUrl)/pay/instore"

    sdkConfig = FramesConfig(
        apiKey: yourAPIkey,
        authToken: "Bearer \(accessToken)",
        apiBase: framesApiBaseUrl,
        logLevel: LogLevel.DEBUG
    )
    //..
}

@IBOutlet weak var framesHost: FramesView!
let CAPTURE_CARD_ACTION = "cardCapture"

func cardCaptureCommand(options: CaptureCard.Payload) throws -> JavascriptCommand {
    try BuildFramesCommand(commands:
        CaptureCard(payload: options).toCommand(name: CAPTURE_CARD_ACTION),
        StartActionCommand(name: CAPTURE_CARD_ACTION)
    )
}

// onPageLoaded
cardCaptureOptions = CardCaptureOptions(
    wallet: Wallet.MERCHANT,
    require3DS: false
)

cardCaptureCommand(options: cardCaptureOptions!).post(view: framesHost)

Where:

  • the package.json file should contain the following dependencies:
  "@api-sdk-creator/axios-http-client": "^0.1.7",
  "@types/typescript": "^2.0.0",
  "@wpay/frames": "^2.0.3",
  "@wpay/sdk": "^1.8.7"

Card Capture Action Options

Save Card

By default, the card will attempt to save to the customer's wallet unless otherwise specified. If you would like to specify whether to save the card to the customer's wallet on tokenization the save property can be passed as part of the options when initialising the capture card action.

The 'save' option can be overridden during the tokenize step should you wish to change this after the frame has already been initialised. For example, where you provide a save check-box which the customer can select based on their preference to save the card to their wallet for easier future check-out or not. This is done during the completion call.

framesSDK.createAction(
  frames.ActionTypes.CaptureCard, 
  { save: true }
);
fun cardCaptureOptions(options: CardCaptureOptions) =
    ActionType.CaptureCard.Payload(
        save = true
    )
static func cardCaptureOptions(options: CardCaptureOptions) -> CaptureCard.Payload {
		CaptureCard.Payload(
			save: true
		)
	}

Verify Card

Card verification is the process of performing a 1c pre-auth on the card at the point of tokenizing to ensure that the card is valid.

Card verification should be done when you are not wanting to immediately process a payment following the saving of a card. For example, in a use case where the customer can add and remove cards from their wallet without having to make an immediate payment.

🚧

Verification Consumes the Step Up Token

When a 1c pre-auth verification is done as part of tokenizing a card it will consume the step up token generated as part of the card capture. Should you try and verify a card and immediately process a payment you will be required to capture the CVV again. Should you be processing a payment with a card immediately after tokenizing it you should set verify to false.

By default, the card verification is disabled on card capture. If you would like to capture a card and enforce verification the verify property can be passed as part of the options when initialising the capture card action.

framesSDK.createAction(
	frames.ActionTypes.CaptureCard, 
  { verify: true }
);
fun cardCaptureOptions(options: CardCaptureOptions) =
    ActionType.CaptureCard.Payload(
        verify = true,
    )
static func cardCaptureOptions(options: CardCaptureOptions) -> CaptureCard.Payload {
		CaptureCard.Payload(
			verify: true
		)
	}

An example of how to set multiple optional parameters (save and verify) when initiating the frame.

import * as frames from '@wpay/frames';

import { ApiTokenType } from '@wpay/sdk';
import * as frames from '@wpay/frames';

const apiKey = 'YOUR-API-KEY';
const authorizationToken: ApiTokenType = 'xxx'; // Obtained via serverside API token endpoint
const environment = 'pt-api'
const baseUrl = `https://${environment}.wpay.com.au/wow/v1`;
const framesApiBaseUrl = `${baseUrl}/pay/instore`;

//Instantiate the frames SDK, this will allow us to capture user card infromation.
const framesSDK = new frames.ElementsSDK(
    apiKey,
    `Bearer ${authorizationToken}`,
    framesApiBaseUrl,
    frames.LogLevel.DEBUG
);

framesSDK.createAction(
  frames.ActionTypes.CaptureCard, 
  { 
    save: true,
  	verify: false
  }
);
fun cardCaptureOptions(options: CardCaptureOptions) =
    ActionType.CaptureCard.Payload(
        verify = true,
        save = true,
    )
static func cardCaptureOptions(options: CardCaptureOptions) -> CaptureCard.Payload {
		CaptureCard.Payload(
			verify: true,
      save: true
		)
	}

Host the Frames

Multi Line Frame:

The multi-line frame is actually comprised of separate elements which can be arranged as required to achieve the desired look and feel for your site. In order, to generate the multi-line frame you will need to generate each element as shown here:

// Link the HTML cardCapture ids: `cardCaptureCardNo`, `cardCaptureExpiry` and `cardCaptureCVV`
// with their respective frames SDK fields: `CardNo`, `CardExpiry` and `CardCVV`
action.createFramesControl('CardNo', 'cardCaptureCardNo', options);
action.createFramesControl('CardExpiry', 'cardCaptureExpiry', options);
action.createFramesControl('CardCVV', 'cardCaptureCVV', options);
<!DOCTYPE html>
<html>
    <head>
        <style>
            .card-capture-container {
                display: flex;
                flex-direction: column;
            }
            #cardCapturePlaceholder {
                height: 50px;
                position: relative;
            }
            #cardCapturePlaceholder .woolies-element {
                width: 99%;
            }
            #cardCapturePlaceholder iframe {
                height: 37px !important;
                border: solid 1px #777;
                padding: 5px;
                border-radius: 5px;
            }
            #cardCaptureErrors {
                height: 30px;
                color: red;
            }
        </style>
        <link rel="stylesheet" href="atom-one-dark.css">
    </head>
    <body>
      <script type="module" src="./client.js"></script>
      <div class="card-capture-container">
        
        <!-- 
      		Card capture placehold div for each multi element: 
        		- The Frames SDK will fill this element with the card capture frames.
        -->
        <div id="cardCaptureCardNo"></div>
        <div id="cardCaptureExpiry"></div>
        <div id="cardCaptureCVV"></div>

        <!--
          Placeholder for errors returned by eventListner to Frames SDK 
          onValidated events.
        -->
        <div id="cardCaptureErrors"></div>
      </div>
      <div style="text-align: right; width: 100%;">
        <!-- Submit button for the card capture -->
        <button id="submitCard" class="btn" disabled>Submit</button>
      </div>
      <!-- Example payment section -->
      <div id="paymentSection" class="section center collapsible-section">
        <p><b>Payment: </b> $100</p>
        <p><b>Payment Instrument:</b> <span id="instrumentIdDisplay"></span></p>
        <p><b>Payment Request:</b> <span id="paymentRequestDisplay"></span></p>
        <button id="makePayment">Make Payment</button>
      </div>
      <div id="responseSection" class="section center collapsible-section">
        <p><b>Payment Response: </b></p>
      </div>
    </body>
</html>
import androidx.lifecycle.MutableLiveData

import au.com.wpay.frames.JavascriptCommand
import au.com.wpay.frames.BuildFramesCommand
import au.com.wpay.frames.CreateActionControlCommand

import au.com.wpay.frames.types.ControlType

val framesCommand: MutableLiveData<JavascriptCommand?> = MutableLiveData(null)

const val CAPTURE_CARD_ACTION = "cardCapture"
const val CARD_NO_DOM_ID = "cardCaptureCardNo"
const val CARD_EXPIRY_DOM_ID = "cardCaptureExpiry"
const val CARD_CVV_DOM_ID = "cardCaptureCVV"

framesCommand.postValue(BuildFramesCommand(
  CreateActionControlCommand(CAPTURE_CARD_ACTION, ControlType.CARD_NUMBER, CARD_NO_DOM_ID),
  CreateActionControlCommand(CAPTURE_CARD_ACTION, ControlType.CARD_EXPIRY, CARD_EXPIRY_DOM_ID),
  CreateActionControlCommand(CAPTURE_CARD_ACTION, ControlType.CARD_CVV, CARD_CVV_DOM_ID)
))
import WPayFramesSDK

let CAPTURE_CARD_ACTION = "cardCapture"
let CARD_NO_DOM_ID = "cardCaptureCardNo"
let CARD_EXPIRY_DOM_ID = "cardCaptureExpiry"
let CARD_CVV_DOM_ID = "cardCaptureCVV"

public enum ControlType: String {
	case CARD_GROUP = "CardGroup"
	case CARD_NUMBER = "CardNo"
	case CARD_EXPIRY = "CardExpiry"
	case CARD_CVV = "CardCVV"
	case VALIDATE_CARD = "ValidateCard"
}

static func multiLineCardCaptureCommand() throws -> JavascriptCommand {
	try BuildFramesCommand(commands:
		CreateActionControlCommand(
			actionName: CAPTURE_CARD_ACTION,
			controlType: ControlType.CARD_NUMBER,
			domId: CARD_NO_DOM_ID
		),
		CreateActionControlCommand(
			actionName: CAPTURE_CARD_ACTION,
			controlType: ControlType.CARD_EXPIRY,
			domId: CARD_EXPIRY_DOM_ID
		),
		CreateActionControlCommand(
			actionName: CAPTURE_CARD_ACTION,
			controlType: ControlType.CARD_CVV,
			domId: CARD_CVV_DOM_ID
		)
	)
}

multiLineCardCaptureCommand()

Single Line Frame

The single-line frame is a single element that allows credit card capture but is limited in how the fields can be ordered. In order, to generate the single-line frame you will need to generate the card group as shown here:

// Populate the HTML placeholder div id `cardCapturePlaceholder` 
// with the card capture inputs fields using the `CardGroup`
action.createFramesControl('CardGroup', 'cardCapturePlaceholder');
<!DOCTYPE html>
<html>
    <head>
        <style>
            .card-capture-container {
                display: flex;
                flex-direction: column;
            }
            #cardCapturePlaceholder {
                height: 50px;
                position: relative;
            }
            #cardCapturePlaceholder .woolies-element {
                width: 99%;
            }
            #cardCapturePlaceholder iframe {
                height: 37px !important;
                border: solid 1px #777;
                padding: 5px;
                border-radius: 5px;
            }
            #cardCaptureErrors {
                height: 30px;
                color: red;
            }
        </style>
        <link rel="stylesheet" href="atom-one-dark.css">
    </head>
    <body>
      <script type="module" src="./client.js"></script>
      <div class="card-capture-container">
        <!-- 
      		Card capture placehold div 
        		- The Frames SDK will fill this element with the card capture frames.
        -->
        <div id="cardCapturePlaceholder"></div>
        
        <!--
          Placeholder for errors returned by eventListner to Frames SDK 
          onValidated events.
        -->
        <div id="cardCaptureErrors"></div>
      </div>
      <div style="text-align: right; width: 100%;">
        <!-- Submit button for the card capture -->
        <button id="submitCard" class="btn" disabled>Submit</button>
      </div>
      <!-- Example payment section -->
      <div id="paymentSection" class="section center collapsible-section">
        <p><b>Payment: </b> $100</p>
        <p><b>Payment Instrument:</b> <span id="instrumentIdDisplay"></span></p>
        <p><b>Payment Request:</b> <span id="paymentRequestDisplay"></span></p>
        <button id="makePayment">Make Payment</button>
      </div>
      <div id="responseSection" class="section center collapsible-section">
        <p><b>Payment Response: </b></p>
      </div>
    </body>
</html>
import androidx.lifecycle.MutableLiveData

import au.com.wpay.frames.JavascriptCommand
import au.com.wpay.frames.BuildFramesCommand
import au.com.wpay.frames.CreateActionControlCommand

import au.com.wpay.frames.types.ControlType

val framesCommand: MutableLiveData<JavascriptCommand?> = MutableLiveData(null)

const val CAPTURE_CARD_ACTION = "cardCapture"
const val CARD_GROUP_ID = "cardCapturePlaceholder"

framesCommand.postValue(BuildFramesCommand(
    CreateActionControlCommand(CAPTURE_CARD_ACTION, ControlType.CARD_GROUP, CARD_GROUP_ID)
))
import WPayFramesSDK

let CAPTURE_CARD_ACTION = "cardCapture"
let CARD_CAPTURE_DOM_ID = "cardCapturePlaceholder"

static func singleLineCardCaptureCommand() throws -> JavascriptCommand {
	try BuildFramesCommand(commands:
		CreateActionControlCommand(
			actionName: CAPTURE_CARD_ACTION,
			controlType: ControlType.CARD_GROUP,
			domId: CARD_CAPTURE_DOM_ID
		)
	)
}

singleLineCardCaptureCommand()

Autofill for Card Capture fields using Single or Multi Line Frame

  • Single and multi frame card capture support autofill out of the box with out the need for the developer to do anything extra.
  • When using a signed certificate on a secure site autofill will work for the Card PAN, Expiry Month and Expiry Year.
  • If using a card that is attached to a google pay wallet then autofill will populate the CVV.
  • When using a development or local insecure environment you will need to use a self signed certificate to enable autofill to work.

Frame Events & Methods

Frames Events

The Card Capture Frames listens to onFocus and onBlur events fired from the frames SDK to display validation errors and to provide additional information to cater for your specific use case. For example, enabling a "pay" button once all fields are complete you need to know when controls are visited.

If you would like to listen in to these events you can do so by adding an event listener to the placeholder element in much the same way as you do for validation. e.g.
document .getElementById('cardCaptureCardNo') .addEventListener( Frames.FramesEventType.OnBlur, () => { // Do something onBlur } );

In the below example the submit button is enabled when all the credit card fields have been visited and there is no current validation error:

import { createAxiosHttpClient } from '@api-sdk-creator/axios-http-client';
import { 
  ApiTokenType, createCustomerSDK, WPayCustomerApi, WPayCustomerOptions 
} from '@wpay/sdk';

const apiKey = 'YOUR-API-KEY';
const authorizationToken: ApiTokenType = 'xxx'; // Obtained via serverside API token endpoint
const environment = 'pt-api';
const walletApiBaseUrl = `https://${environment}.wpay.com.au/wow/v1/pay`;

let action: any;
let customerSDK: WPayCustomerApi;
let submitCardBtn: HTMLButtonElement;
let makePaymentBtn: HTMLButtonElement;

function setupForCardCapture() {
  //Instantiate the customer API
  const options: WPayCustomerOptions = {
    apiKey: apiKey,
    baseUrl: walletApiBaseUrl,
    accessToken: authorizationToken,
  };
  customerSDK = createCustomerSDK(createAxiosHttpClient, options);

  // Once the page has loaded, initialise a new card capture action.
  action = framesSDK.createAction(frames.ActionTypes.CaptureCard);
  await action.start();

  // Populate the placeholder div with the card capture inputs. In this case we
  // are using the 'CardGroup'
  action.createFramesControlcreateFramesControlcreateElement('CardGroup', 'cardCapturePlaceholder');
  
  // Add OnValidated Eventlistener which will cause the updateErrors
  // function to be called if a validation error is encoutned in the
  // frames SDK while entering a Credit Card details.
  document.getElementById('cardCapturePlaceholder')!
    .addEventListener(
      frames.FramesEventType.OnValidated,
      updateErrors
    );

  // Add OnFocus Eventlistener which is fired when a field is focused in the frames SDK.
  // This enables you to know if all fields have been visited and the credit card is ready
  // for submission.
  document.getElementById('cardCapturePlaceholder')!
    .addEventListener(
      frames.FramesEventType.OnFocus,
      setVisitedStatus
    );

  // Add OnBlur Eventlistener which is fired when a field is exited in the frames SDK.
  // This enables you to check for errors and visited fields to see if the credit card 
  // is ready for submission.
  document.getElementById('cardCapturePlaceholder')!
    .addEventListener(
      frames.FramesEventType.OnBlur,
      checkVisitedStatus
    );

  submitCardBtn = document.getElementById('submitCard') as HTMLButtonElement;
  makePaymentBtn = document.getElementById(
    'makePayment'
  ) as HTMLButtonElement;
  submitCardBtn.onclick = captureCard;
	makePaymentBtn.onclick = makePayment;
}
class PaymentSimulatorModel :
	FramesView.Callback
  //, ... plus other interfaces to implement
{
  //...
  var enableSaveButton = false

  override fun onValidationChange(domId: String, isValid: Boolean) {
    debug("onValidationChange($domId, isValid: $isValid)")
    setErrorStatus(domId, isValid)
  }

  override fun onFocusChange(domId: String, isFocussed: Boolean) {
    debug("onFocusChange($domId, isFocussed: $isFocussed)")
    setVisitedStatus(domId)
    // !isFocussed is equivalent to onBlur
    if (!isFocussed && checkVisitedStatus() && checkErrorStatus()) {
      enableSaveButton = true
    } else if (!checkErrorStatus()) {
      enableSaveButton = false
    }
  }

  // Could update your own html element, instead of using
  // the default frames error display as shown below:
  override fun onError(error: Exception) {
    error.postValue(error)
  }
  //..
}
import WPayFramesSDK

class PaymentDetails :
	FramesViewCallback
	//, ... plus other interfaces to implement
{
	//...
	var enableSaveButton = false

	func onValidationChange(domId: String, isValid: Bool) {
		debug(message: "onValidationChange(\(domId), isValid: \(isValid))")
		setErrorStatus(domId, isValid)
	}

	func onFocusChange(domId: String, isFocussed: Bool) {
		debug(message: "onFocusChange(\(domId), isFocussed: \(isFocussed))")
		setVisitedStatus(domId)
		// !isFocussed is equivalent to onBlur
		if (!isFocussed && checkVisitedStatus() && checkErrorStatus()) {
			enableSaveButton = true
		} else if (!checkErrorStatus()) {
			enableSaveButton = false
		}
	}

	// Could update your own html element, instead of using
	// the default frames error display as shown below:
	func onError(error: FramesErrors) {
		debug(message: "onError(error: \(error))")

		switch error {
			case .FATAL_ERROR(let message): framesMessage.text = message
			case .NETWORK_ERROR(let message): framesMessage.text = message
			case .TIMEOUT_ERROR(let message): framesMessage.text = message
			case .FORM_ERROR(let message): framesMessage.text = message
			case .EVAL_ERROR(let message): framesMessage.text = message
			case .DECODE_JSON_ERROR(let message, _, _): framesMessage.text = message
			case .ENCODE_JSON_ERROR(let message, _, _): framesMessage.text = message
			case .SDK_INIT_ERROR(let message, _): framesMessage.text = message
	}
	//..
}

Where:

  • createAxiosHttpClient is imported from the node module @api-sdk-creator/axios-http-client and is used to create createCustomerSDK (a custom instance of the @wpay/sdk)
  • updateErrors is an error handling function an example implementation is provided in the Error Handling section below.

Frames Methods

Additional methods on the action that might be useful:

  • The errors method will provide a list of errors in the event validation failed on one or more of the card capture fields.
  • The clear method will allow the user to clear all of the card capture fields.

Error Handling

These the predetermined error events that get raised from the frames SDK:

Frames SDK Error returnedError Description
Card No. RequiredThe card number is missing.
Invalid Card NoThe card number is not valid and has failed length and LUHN checks.
Invalid ExpiryThe expiry date is invalid and does not match the required date format.
Incomplete ExpiryThe expiry date is missing or incomplete.
Expired cardThe expiry date is in the past.
Invalid CVVThe CVV is missing or invalid.

The below example shows an application of the frames validations typically applied during the card capture process and applies logic to determine if all fields have been visited before enabling the Save button.

const errorMap: Map<string, string> = new Map([
    ['Card No. Required', 'Please enter a valid card number.'],
    ['Invalid Card No.', 'Please enter a valid card number.'],
    ['Invalid Expiry', 'Please enter a valid expiry.'],
    ['Incomplete Expiry', 'Please enter a valid expiry'],
    ['Expired card', 'The expiry entered is in the past. Please enter a valid expiry.'],
    ['Invalid CVV', 'Please enter a valid CVV.']
]);

async function updateErrors() {
  const errors = action.errors();
  if (errors !== undefined && errors.length > 0) { 
    // Display the validation error that has occurred.
    document.getElementById('cardCaptureErrors')!.innerHTML =
      `${errorMap.get(errors[0]) ? errorMap.get(errors[0]) : errors[0]}`;
  } else {
    // No validation error has occurred so clear the any error message.
    document.getElementById('cardCaptureErrors')!.innerHTML = "";
  }
}

const visitedStatus: any = {}
let enableSaveButton = false;

async function setVisitedStatus(event: any) {
    // When the cursor enters a field set the vistedStatus for this field to true.
    if (event && event.detail && event.detail.control) {
        visitedStatus[event.detail.control] = true;
        checkVisitedStatus();
    };
}

async function checkVisitedStatus() {
    const keys = [ 'cardNo', 'cardExpiry', 'cardCVV'];
    for (const key of keys) {
        if (!visitedStatus[key]) return;
    }
    if (action.errors() === undefined || action.errors().length === 0) {
        enableSaveButton = true;
    } else {
        enableSaveButton = false;
    }
    (document.getElementById("submitCard")! as HTMLButtonElement).disabled = !enableSaveButton;
}
class PaymentSimulatorModel : // ... interfaces to implement
{
  //...
  var errorStatus: MutableMap<String,Boolean> = mutableMapOf()
  var visitedStatus: MutableMap<String,Boolean> = mutableMapOf()

  fun setVisitedStatus(domId: String) {
      visitedStatus.put(domId, true)
  }

  // Return true if all 3 cardCapture have been visited
  fun checkVisitedStatus(): Boolean {
      return visitedStatus.filterValues { it == true }.count() == 3
  }

  // Set the errorStatus of a field
  fun setErrorStatus(domId: String, isValid: Boolean) {
      errorStatus.put(domId, !isValid)
  }

  // Return true if all 3 cardCapture fields are valid (i.e. their errorStatus is false)
  fun checkErrorStatus(): Boolean {
    return errorStatus.filterValues { it == false }.count() == 3
  }
  //..
}
class PaymentSimulatorModel : // ... interfaces to implement
{
  //...
  var errorStatus = [String:Bool]()
  var visitedStatus = [String:Bool]()

  fun setVisitedStatus(domId: String) {
      visitedStatus[domId] = true
  }

  // Return true if all 3 cardCapture have been visited
  fun checkVisitedStatus(): Bool {
      return visitedStatus.filter { $0 == true }.count == 3
  }

  // Set the errorStatus of a field
  fun setErrorStatus(domId: String, isValid: Boolean) {
      errorStatus[domId] = !isValid
  }

  // Return true if all 3 cardCapture fields are valid (i.e. their errorStatus is false)
  fun checkErrorStatus(): Bool {
    return errorStatus.filter { $0 == false }.count == 3
  }
  //..
}

Tokenize a Captured Card

In order to tokenize a captured card, you will need to call the submit action on each of the created elements and then call the complete action to tokenize the card.

Depending on the action options set when creating the capture action the card will tokenize and potentially perform a 1c verification and save to the customer's wallet.

Choosing whether to save the card details captured during card tokenization

At the point that the card is ready to be tokenized you can specify whether you wish to save the card to the customer's wallet or not by passing in the optional save parameter to the complete action. If the optional save parameter is not passed then the value set when initialising the card capture action will be used.

Submit Card for tokenization

let saveCapturedCard = false;
 await action.submit();

 //If the optional parameter saveCapturedCard is not passed,
 //the value set when initialising the card capture action will be used. 
 const completeResponse = await action.complete(saveCapturedCard);
package au.com.wpay.frames

    //...

    @JavascriptInterface
    fun handleOnComplete(jsonString: String) {
        log("handleOnComplete($jsonString)")

        post {
            callback?.onComplete(jsonString)
        }
    }

package au.com.wpay.sdk.paymentsimulator

import au.com.wpay.frames.*
import au.com.wpay.frames.dto.CardCaptureResponse
import au.com.wpay.frames.dto.ThreeDSError

sealed class PaymentOptions {
    data class NewCard(val valid: Boolean) : PaymentOptions() {
        override fun isValid(): Boolean {
            return valid
        }
    }

    data class ExistingCard(val card: CreditCard?) : PaymentOptions() {
        override fun isValid(): Boolean {
            return card != null
        }
    }

    object NoOption : PaymentOptions() {
        override fun isValid(): Boolean = false
    }

    abstract fun isValid(): Boolean
}

const val CAPTURE_CARD_ACTION = "cardCapture"

class PaymentSimulatorModel :
    FramesView.Callback
    //, ... plus other interfaces to implement
{

    //...

    private fun onCaptureCard(data: String) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                val response = CardCaptureResponse.fromJson(data)
                val instrumentId =
                    when {
                        (response.itemId != null && response.itemId != "") -> {
                            response.itemId
                        }
                        else -> {
                            response.paymentInstrument?.itemId
                        }
                    }

                if (response.message == "3DS Validation Rejected" ||
                    response.message == "3DS Validation Failed" ||
                    response.message == "3DS Validation Timeout") {
                    failPayment(Exception(response.message))
                }

                if (response.threeDSError == ThreeDSError.TOKEN_REQUIRED) {
                    validateCard(response.threeDSToken!!)
                }

                if (response.threeDSError == ThreeDSError.VALIDATION_FAILED) {
                    failPayment(Exception("Three DS Validation Failed"))
                }

                if (response.status?.responseText == "ACCEPTED") {
                    val cards = listPaymentInstruments()
                    val card = cards?.find { it.paymentInstrumentId == instrumentId }

                    makePayment(PaymentOptions.ExistingCard(card))
                }
            }
        }
    }

    override fun onComplete(response: String) {
        debug("onComplete(response: $response)")

        try {
            framesActionHandler(response)
        }
        catch (e: Exception) {
            onError(e)
        }
    }

    private fun completeCapturingCard() {
        validCardAttemptCounter = 0

        framesCommand.postValue(SubmitFormCommand(CAPTURE_CARD_ACTION))
    }
}

/*
	Where the order of invocation for a `3DS` `Card Capture` in `Kotlin` is as following:

	1. `completeCapturingCard`
	2. `framesCommand.postValue(SubmitFormCommand(CAPTURE_CARD_ACTION))`
	3. `onComplete(response: String)`
	4. `onCaptureCard(_ data: String)`
*/
import WPayFramesSDK

public class SubmitFormCommand : JavascriptCommand {
	public init(name: String) {
		super.init(command:
			"""
			frames.submit = async function() {
			    try {
			        await this.actions.\(name).submit()

			        const response = await this.actions.\(name).complete()
			        window.webkit.messageHandlers.handleOnComplete.postMessage(JSON.stringify(response))
			    }
			    catch(e) {
			        frames.handleError('submit', e)
			    }
			}

			frames.submit();

			true
			"""
		)
	}
}

public class FramesView : WKWebView, WKScriptMessageHandler {
	//..
	private func handleOnComplete(message: WKScriptMessage) {
		let jsonString = message.body as! String

		log("handleOnComplete(\(jsonString))")

		callback?.onComplete(response: jsonString)
	}
}

class PaymentDetails :
	FramesViewCallback
	//, ... plus other interfaces to implement
{
	//...
	@IBOutlet weak var framesHost: FramesView!

	private var framesActionHandler: FramesActionHandler?

	let CAPTURE_CARD_ACTION = "cardCapture"

	private func onCaptureCard(_ data: String) -> Void {
		do {
			let response = try CardCaptureResponse.fromJson(json: data)!
			var instrumentId: String?

			if (response.itemId != nil && response.itemId != "") {
				instrumentId = response.itemId
			}
			else {
				instrumentId = response.paymentInstrument?.itemId
			}

			if (response.message == "3DS Validation Rejected" ||
					response.message == "3DS Validation Failed" ||
					response.message == "3DS Validation Timeout") {
					failPayment(reason: response.message!)
			}

			if (response.threeDSError == ThreeDSError.TOKEN_REQUIRED) {
				validateCard(threeDSToken: response.threeDSToken!)
			}

			if (response.threeDSError == ThreeDSError.VALIDATION_FAILED) {
				failPayment(reason: "Three DS Validation Failed")
			}

			if (response.status?.responseText == "ACCEPTED") {
				reloadPaymentInstruments {
					let card = self.appDelegate.paymentInstruments?.first(where: { card in
						card.paymentInstrumentId == instrumentId
					})

					self.paymentOption = .existingCard(card: card)
					self.makePaymentUsingOption()
				}
			}
		}
		catch {
			failPayment(error: error as! FramesErrors)
		}
	}

	func onComplete(response: String) {
		debug(message: "onComplete(response: \(response))")

		framesActionHandler!(response)
	}

	private func completeCapturingCard() {
		validCardAttemptCounter = 0

		SubmitFormCommand(name: CAPTURE_CARD_ACTION).post(view: framesHost)
	}
}

/*
	Where the order of invocation for a `3DS` `Card Capture` in `Swift` is as following:

	1. `completeCapturingCard`
	2. `SubmitFormCommand(name: CAPTURE_CARD_ACTION).post(view: framesHost)`
	3. `onComplete(response: String)`
	4. `onCaptureCard(_ data: String)`
*/

Example of full card capture with CVV Step Up Frames Primed

let tokenizedInstrument: any;

async function captureCard(saveCapturedCard: boolean = false) {
    //Capture the card inputted by the user
    try {
        //Submit the card capture inputs for tokenization
        await action.submit();
      
        //If the optional parameter saveCapturedCard is not passed it will be true by default.
        const completeResponse = await action.complete(saveCapturedCard);

        if (completeResponse.paymentInstrument) {
            //A new instrument has been tokenized
            tokenizedInstrument = {
                paymentInstrumentId:
                    completeResponse.paymentInstrument.itemId,
                stepUpToken: completeResponse.stepUpToken,
            };
        } else {
            //The instrument was already found to exist
            tokenizedInstrument = {
                paymentInstrumentId: completeResponse.itemId,
                stepUpToken: completeResponse.stepUpToken,
            };
        }

        // Display the payment section
        submitCardBtn.disabled = true;
        document.getElementById('instrumentIdDisplay')!.innerText =
            tokenizedInstrument.paymentInstrumentId;
        displaySection('paymentSection');
        console.log('Instrument details: ', tokenizedInstrument);
    } catch (error) {
        console.log('Error during card capture: ', error);
    }
}
package au.com.wpay.frames

const val CAPTURE_CARD_ACTION = "cardCapture"
const val VALIDATE_CARD_ACTION = "validateCard"
const val VALIDATE_CARD_DOM_ID = "validateCardElement"

fun cardValidateCommand(
    sessionId: String,
    windowSize: ActionType.AcsWindowSize
): JavascriptCommand =
    GroupCommand("validateCard",
        ActionType.ValidateCard(validateCardOptions(sessionId, windowSize)).toCommand(VALIDATE_CARD_ACTION),
        StartActionCommand(VALIDATE_CARD_ACTION),
        CreateActionControlCommand(VALIDATE_CARD_ACTION, ControlType.VALIDATE_CARD, VALIDATE_CARD_DOM_ID),
        CompleteActionCommand(VALIDATE_CARD_ACTION)
    )

class PaymentSimulatorModel :
    FramesView.Callback
    //, ... plus other interfaces to implement
{

    //...

    private fun onValidateCard(data: String) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                val response = ValidateCardResponse.fromJson(data)

                framesActionHandler = this@PaymentSimulatorModel::onCaptureCard

                framesCommand.postValue(GroupCommand("completeCardCapture",
                    CompleteActionCommand(CAPTURE_CARD_ACTION, cardCaptureOptions.save, JSONArray().apply {
                        response.challengeResponse?.let { put(it.toJson()) }
                    })
                ))
            }
        }
    }

    private fun validateCard(threeDSToken: String) {
        if (validCardAttemptCounter > 1) {
            failPayment(Exception("Validate card attempt counter exceeded"))
        }
        else {
            validCardAttemptCounter++

            framesActionHandler = ::onValidateCard

            framesCommand.postValue(cardValidateCommand(threeDSToken, windowSize))
        }
    }

}
/*
	Where the order of invocation for a `3DS` `Card Capture` in `Kotlin` is as following:

	1. `completeCapturingCard`
	2. `framesCommand.postValue(SubmitFormCommand(CAPTURE_CARD_ACTION))`
	3. `onComplete(response: String)`
	4. `onCaptureCard(_ data: String)`

	Incase of `3DS` Card Capture in Kotlin the following steps are require in this order:

	5. `validateCard(threeDSToken: String)`
	6. `onValidateCard(data: String)`
	7. `onCaptureCard(_ data: String)`
*/
import WPayFramesSDK

class PaymentDetails :
	FramesViewCallback
	//, ... plus other interfaces to implement
{
	//...
	@IBOutlet weak var framesHost: FramesView!

	private var framesActionHandler: FramesActionHandler?

	let CAPTURE_CARD_ACTION = "cardCapture"

	private func onValidateCard(data: String) {
		do {
			let response = try ValidateCardResponse.fromJson(json: data)!
			var challengeResponse: [WPayFramesSDK.ChallengeResponse] = []

			if let challenge = response.challengeResponse {
				challengeResponse.append(challenge)
			}

			framesActionHandler = onCaptureCard

			GroupCommand(name: "completeCardCapture", commands:
				try CompleteActionCommand(
					name: CAPTURE_CARD_ACTION,
					save: cardCaptureOptions!.save,
					challengeResponses: challengeResponse
				)
			).post(view: framesHost, callback: nil)
		}
		catch {
			failPayment(error: error as! FramesErrors)
		}
	}

	private func validateCard(threeDSToken: String) {
		if (validCardAttemptCounter > 1) {
			failPayment(reason: "Validate card attempt counter exceeded")
		}
		else {
			validCardAttemptCounter = validCardAttemptCounter + 1

			framesActionHandler = onValidateCard

			do {
				try Commands.cardValidateCommand(
					sessionId: threeDSToken,
					windowSize: appDelegate.windowSize!
				).post(view: framesHost)
			}
			catch {
				fatalError("Can't post card validate command")
			}
		}
	}
}
/*
	Where the order of invocation for a `3DS` `Card Capture` in `Swift` is as following:

	1. `completeCapturingCard`
	2. `SubmitFormCommand(name: CAPTURE_CARD_ACTION).post(view: framesHost)`
	3. `onComplete(response: String)`
	4. `onCaptureCard(_ data: String)`

	Incase of `3DS` Card Capture in Swift the following steps are require in this order:

	5. `validateCard(threeDSToken: String)`
	6. `onValidateCard(data: String)`
	7. `onCaptureCard(_ data: String)`
*/

When a card is successfully tokenized the user will receive the new tokenized card information, should the card have already existed in the customers wallet then the existing card information will be returned.

{
  "status": {
   	"responseText": "ACCEPTED",
    "responseCode": "00",
    "auditID": "3f9143d8-d8d1-46df-88b6-e21f0acb66f0",
    "txnTime": 1635230585570,
    "error": null,
    "esResponse": null
  },
  "paymentInstrument": {
    "itemId": "217011",
    "paymentToken": "08935b8a-4f61-46ca-9ba0-661e8474782e",
    "status": "UNVERIFIED_PERSISTENT",
    "created": 1635230585570,
    "bin": "360502",
    "suffix": "0913",
    "expiryMonth": "08",
    "expiryYear": "22",
    "nickname": "",
    "scheme": "DINERS"
  },
    "stepUpToken": "tokenise-stepup-token",
    "fraudResponse": {
    "fraudClientId": null,
    "fraudReasonCd": null,
    "fraudDecision": null
  }
}
{
  "status": {
    "responseText": "ACCEPTED",
    "responseCode": "00",
    "auditID": "c2110136-4c46-4592-b553-92314e89cbb4",
    "txnTime": 1635228471677,
    "error": null,
    "esResponse": {
      "code": "00",
       "text": "ACCEPTED PAYMENT INSTRUMENT ALREADY EXISTS IN ACCOUNT"
     }
  },
  "itemId": "213309",
  "stepUpToken": "tokenise-stepup-token"
}

The itemId can be as the tokenized instrument when making a payment. For more context you can refer to the API guide for Making a Payment.

Step Up Process

Capture CVV

When utilising saved cards from your customer's wallet you will usually want them to capture their CVV and have this CVV information included as part of the payment information. This is known as the step-up process and results in a step up token which can be included as part of your payload when making a payment as part of your challenge response.

Step Up Token

Where a step up token is required your customer will need to capture their CVV as part of a their payment request. The CVV capture will result in a step up token which is a UUID representation of the tokenized CVV which needs to be provided as part of a payment.

📘

3DS Integration

If you are interested in 3DS2, please review 3D Secure (3DS)

How it works

  1. Start a new card step up action
  2. Add the CVV element to the page
  3. Create your frames element - specify the element you would like to create and the id of the dom element that you would like to attach the element to
  4. Create the Frames Controller for the CVV element
  5. Submit the CVV and get the Step Up Token which can then be used to make a credit card payment
/*Start a new card step up action referencing your paymentInstrumentID*/
let action = sdk.createAction(
    FRAMES.ActionTypes.StepUp,
    {
        paymentInstrumentId: <YOUR PAYMENT INSTRUMENT ID>,
        scheme: 'VISA'
    }
);
action.start();

/*This will initialise a new step up action. 
This call will need to be repeated between subsequent step up token requests.*/

action.createFramesControl('CardCVV', 'cardCaptureCVV');

/* Same as in above example submit the CVV capture and make the payment */
submitCardBtn = document.getElementById('submitCard') as HTMLButtonElement;
makePaymentBtn = document.getElementById(
  'makePayment'
) as HTMLButtonElement;

// This is where action.submit() called as defined in the captureCard function above.
submitCardBtn.onclick = captureCard;

// This is where the makePayment function as defined above is linked to the button. 
makePaymentBtn.onclick = makePayment;
<!-- Add the cvv element to the page.
The SDK attaches new elements to div placeholders within your page using the element id.
-->
<div id="cardCaptureCVV"></div>

Where:

  • The scheme should be returned as part of the paymentInstrument when you call the complete method during the initial tokenisation VISA, MASTERCARD, AMEX and DINERS are all valid values.

The high-level flow is shown in the above example to capture a CVV and receive a step up token is:

  • Initialise a new step up action. Note that this call will need to be repeated between subsequent step up token requests.
  • Adding the CVV element to the page i.e. <div id="cardCaptureCVV"></div>. Note that the SDK attaches new elements to div placeholders within your page using the element id
  • After adding your placeholder you can now create your frames element. When creating an element pass in the type of the element you would like to create and the id of the dom element that you would like to attach it to. i.e. action.createFramesControl('CardCVV', 'cardCaptureCVV');. Loading the page should now display the credit card capture element, displaying card, expiry date and CVV.
  • Once the user has entered their CVV, you are going to want to submit and create the step-up token. To do this add a Submit button to the page calling the submit function on the action i.e. <button onClick="async function() { await action.submit()}">Submit</button>
  • Once successfully submitted an action needs to be completed. Do so by calling complete i.e.let stepUpResult = await action.complete();

You should now have a step up token which can be used as part of Making a Payment where required. For more context you can refer to the API guide for Making a Payment.

Logging

If you would like to see what is going on inside of the SDK, you can enable logging using the SDK constructor. Simply set the log level you would like to see and you should be able to see the log output in the console window. The log level is universal so applies to both the SDK and IFrame content.

Log Levels

  • NONE = 0,
  • ERROR = 50,
  • INFO = 100,
  • DEBUG = 200