Integrate Frames
How it works
To integrate Frames into your site there are a few steps that you need to follow:
-
Initialise the frames SDK and host the frames:
- Define a placeholder element where the capture elements will be inserted into the page. This element needs to have an
id
defined. - 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.
- Create the Frames capture element by calling the
createFramesControl
method on the action and passing in the element type and theid
of the DOM element that you would like to attach it to.
- Define a placeholder element where the capture elements will be inserted into the page. This element needs to have an
-
Validate the captured data using events:
- 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.
- 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.
-
Tokenize the captured card:
- 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. - Once a card capture action has been successfully submitted you'll need to complete the action by calling the
complete
method on the action.
- 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
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 createcreateCustomerSDK
(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 returned | Error Description |
---|---|
Card No. Required | The card number is missing. |
Invalid Card No | The card number is not valid and has failed length and LUHN checks. |
Invalid Expiry | The expiry date is invalid and does not match the required date format. |
Incomplete Expiry | The expiry date is missing or incomplete. |
Expired card | The expiry date is in the past. |
Invalid CVV | The 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
- Start a new card step up action
- Add the CVV element to the page
- 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
- Create the Frames Controller for the CVV element
- 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
andDINERS
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
Updated over 1 year ago