3DS Card Capture Integration

3DS Card Capture allows you to have 3DS Authentication performed prior to saving the card into the Customer Wallet for future use.

Steps involved in performing a card capture include:

  1. Create a 3DS enabled card capture action
  2. Perform initial card capture
  3. Check card capture response
    3.1 If we require 3DS, the the action will fail with 3DS_001
    3.1.1 Perform 3DS Card Capture
    In order to provide the 3DS evidence to the tokenisation process, we need to capture 3DS
    3.1.2 If 3DS capture was successful we will have a challenge response,
    3.1.3 If 3DS capture was unsuccessful there is no point trying to tokenise again.
    3.2 If we have an paymentInstrumentId then the capture was successful
  4. Perform 3DS Card Capture
    4.1 Control the visibility of the spinner and 3DS challenge response modal
    4.2 Show the 3DS Card Capture challenge response

1. Create a 3DS enabled card capture action

To initialise a 3DS card capture for a non-frictionless card you specify a threeDS option when creating the CaptureCard action via the Frames SDK as shown in the code snippet below:

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

const framesSDK = new FRAMES.FramesSDK(
  {
    apiKey: process.env.VUE_APP_API_KEY,
    authToken: process.env.VUE_APP_ACCESS_TOKEN,
    apiBase: `${process.env.VUE_APP_BASE_URL}/instore`,
    logLevel: FRAMES.LogLevel.DEBUG,
  },
);

const captureCardAction = framesSDK.createAction(
  FRAMES.ActionTypes.CaptureCard,
  {
    verify: false,
    save: true,
    threeDS: {
      requires3DS: true,
    },
  },
);
import * as FRAMES from '@wpay/frames';

const framesSDK = new FRAMES.FramesSDK(
  {
    apiKey: process.env.VUE_APP_API_KEY,
    authToken: process.env.VUE_APP_ACCESS_TOKEN,
    apiBase: `${process.env.VUE_APP_BASE_URL}/instore`,
    logLevel: FRAMES.LogLevel.DEBUG,
  },
);

const captureCardAction = framesSDK.createAction(
  FRAMES.ActionTypes.CaptureCard,
  ({
    verify: false,
    save: true,
    threeDS: {
      requires3DS: true,
    },
  } as any),
) as CaptureCard;

2. Perform initial card capture

The card capture and iFrame validation of card fields follow the same steps as documented in our iFrames integration guide: Host the Frames

The difference is that the response from the cardCaptureAction.complete() is tested to see whether a 3DS card capture is required:

<div class="container">
  
  <!-- Card Capture iFrame place holder -->
  <div id="cardGroupPlaceholder"></div>
  
</div
/* Card Capture iFram place holder styling */
#cardGroupPlaceholder {
  border: 1px solid;
  padding: 10px;
  min-height: 22px;
  margin-bottom: 15px;
}
await captureCardAction.start();
captureCardAction.createFramesControl('CardGroup', 'cardGroupPlaceholder');

// When the Card Caputure iFrame gets focus change the 
//   state variable paymentMethod to 'enterCardDetails'
document.getElementById('cardGroupPlaceholder') &&
  document.getElementById('cardGroupPlaceholder').addEventListener(
    FRAMES.FramesEventType.OnFocus, () => {
      this.paymentMethod = 'enterCardDetails';
    });

// Log any Card Capture Validation errors
document.getElementById('cardGroupPlaceholder') &&
  document.getElementById('cardGroupPlaceholder').addEventListener(
    FRAMES.FramesEventType.OnValidated, () => {
      console.log(captureCardAction.errors());
    });

// Submit the action
await captureCardAction.submit();

// Call complete for first attempt at tokenisation
let cardCaptureResponse = await captureCardAction.complete();

// Set the selected intrument and stepUp token from the inital card capture
let selectedInstrument = 
    (cardCaptureResponse.paymentInstrument && cardCaptureResponse.paymentInstrument.itemId) || 
    cardCaptureResponse.itemId;
let stepUpToken = cardCaptureResponse.stepUpToken;
await captureCardAction.start();
captureCardAction.createFramesControl('CardGroup', 'cardGroupPlaceholder');

// When the Card Caputure iFrame gets focus change the 
//   state variable paymentMethod to 'enterCardDetails'
document.getElementById('cardGroupPlaceholder')?.addEventListener(
  FRAMES.FramesEventType.OnFocus, () => {
    this.paymentMethod = 'enterCardDetails';
  });

// Log any Card Capture Validation errors
document.getElementById('cardGroupPlaceholder')?.addEventListener(
  FRAMES.FramesEventType.OnValidated, () => {
    console.log(captureCardAction.errors());
  });

// Submit the action
await captureCardAction.submit();

// Call complete for first attempt at tokenisation
let cardCaptureResponse = await captureCardAction.complete();

// Set the selected intrument and stepUp token from the inital card capture
let selectedInstrument = 
    cardCaptureResponse.paymentInstrument?.itemId || cardCaptureResponse.itemId;
let stepUpToken = cardCaptureResponse.stepUpToken;

3. Check card capture response

// Intialise value of the card capture success return variable
let preconditionsMet = false;

// 3.1 If we require 3DS, the the action will fail with 3DS_001 
if (cardCaptureResponse.errorCode === '3DS_001') {

    // 3.1.1 Perform 3DS Card Capture
    // In order to provide the 3DS evidence to the tokenisation process, we need to capture 3DS
    const authorizationResponse = 
        await capture3DS(
            cardCaptureResponse.token, 
            selectedInstrument, 
            FRAMES.ActionTypes.ValidateCard);

    // 3.1.2 If 3DS capture was successful we will have a challenge response, 
    if (authorizationResponse.challengeResponse) {
        cardCaptureResponse = 
          await captureCardAction.complete(
            saveCard, [authorizationResponse.challengeResponse]);
        preconditionsMet = true;
    } else {

        // 3.1.3 If 3DS capture was unsuccessful there is no point trying to tokenise again.
        paymentStatus = 'Payment Failed - There was an issue during card capture';
    }
} else if (cardCaptureResponse.itemId || cardCaptureResponse.paymentInstrument.itemId) {

    // 3.2 If we have an paymentInstrumentId then the capture was successful
    preconditionsMet = true;
}
// Intialise value of the card capture success return variable
let preconditionsMet = false;

// 3.1 If we require 3DS, the the action will fail with 3DS_001 
if (cardCaptureResponse.errorCode === '3DS_001') {

    // 3.1.1 Perform 3DS Card Capture
    // In order to provide the 3DS evidence to the tokenisation process, we need to capture 3DS
    const authorizationResponse = 
        await this.capture3DS(
            cardCaptureResponse.token, 
            selectedInstrument, 
            FRAMES.ActionTypes.ValidateCard);

    // 3.1.2 If 3DS capture was successful we will have a challenge response, 
    if (authorizationResponse.challengeResponse) {
        cardCaptureResponse = 
          await captureCardAction.complete(
            this.saveCard, [authorizationResponse.challengeResponse]);
        preconditionsMet = true;
    } else {

        // 3.1.3 If 3DS capture was unsuccessful there is no point trying to tokenise again.
        this.paymentStatus = 'Payment Failed - There was an issue during card capture';
    }
} else if (cardCaptureResponse.itemId || cardCaptureResponse.paymentInstrument.itemId) {

    // 3.2 If we have an paymentInstrumentId then the capture was successful
    preconditionsMet = true;
}

Where:

  • cardCaptureResponse.errorCode has values as defined in the section 3DS error codes
  • saveCard is a boolean value of true to save the card, false to not save the card or undefined if the we want to use the value set when initialising the card caption action

4. Perform 3DS Card Capture

4.1 Control the visibility of the spinner and 3DS challenge response modal

Controlling the visibility of the 3DS Card Capture Challenge Response Model. After validation of the card entered by the user the showSpinner is set to try showing the spinner.

<div class="container">
  <!-- 
       3DS iFrame Challenge response placeholer.
       Includes a Vue.js class which controls the visible of this model, 
       based on the value of the show3DS variable.
  -->
  <div 
       id="overlay" 
       class="overlay" 
       style="" 
       v-bind:class="{ hidden: !this.show3DS }">
  </div>
  
  <!-- Card Capture iFrame place holder -->
  <div id="cardGroupPlaceholder"></div>
  
  <!--
      Display the Spinner component, when 
        the paymentDisabled variable is true or 
        the showSpinner variable is true.
  -->
  <div class="processing" 
       v-bind:class="{ hidden: !this.paymentDisabled || this.showSpinner === false }">
    <Spinner/>
  </div>
</div>
/*
  You provide can style the 3DS Challenge reponse iframe model
  with custom css as shown below:
*/
.overlay {
  position: fixed; top: 0; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.5); 
  display: flex; 
  flex-direction: column; 
  align-items: center;
  z-index: 1; 
  justify-content: center;
}

.overlay iframe {
  background: white;
  padding: 5px;
  border-radius: 5px;
  border: 1px solid black;
}

/* Positioning the Spinner component. */
.processing {
  text-align: center;
}
this.showSpinner = true;

private async capture3DS(sessionId, paymentInstrumentId, actionType) {
  const enrollmentRequest = {
    sessionId,
    paymentInstrumentId,
    threeDS: {
      consumerAuthenticationInformation: {
        acsWindowSize: settings.customer.acsWindowSize,
      },
    },
  };

  // Create a new payment validation frames action
  const action = framesSDK.createAction(actionType, enrollmentRequest);

  // Start the action, creating a new JWT and initialising cardinal
  await action.start();

  // Set the placeholder for the challenge IFrame to be injected
  action.createFramesControl('3DSValidation', 'overlay');

  const elementHandle = document.getElementById('overlay');

  const renderEventListener = () => {
    this.showSpinner = false;
    this.show3DS = true;
  };

  const closeEventListener = () => {
    this.showSpinner = true;
    this.show3DS = false;
  };

  // Add the event listeners for OnRender and OnClose for the 3DS challenge response
  elementHandle.addEventListener(FRAMES.FramesCardinalEventType.OnRender, renderEventListener);
  elementHandle.addEventListener(FRAMES.FramesCardinalEventType.OnClose, closeEventListener);

  // Check card enrolment, allowing cardinal show issuer challenge
  const authorizationResponse = await action.complete();

  // Romove the event listeners for OnRender and OnClose for the 3DS challenge response
  elementHandle.removeEventListener(FRAMES.FramesCardinalEventType.OnRender, renderEventListener);
  elementHandle.removeEventListener(FRAMES.FramesCardinalEventType.OnClose, closeEventListener);

  // 3DS check complete, use returned information to provide a challenge response within the payment endpoint
  console.log(`3DS authorization complete: ${JSON.stringify(authorizationResponse)}`);

  if (actionType === FRAMES.ActionTypes.ValidatePayment) {
    paymentAuthentication = authorizationResponse;
  } else if (actionType === FRAMES.ActionTypes.ValidateCard) {
    cardValidation = authorizationResponse;
  }

  return authorizationResponse;
}
this.showSpinner = true;

private async capture3DS(sessionId: string, paymentInstrumentId: string, actionType: symbol) {
  const enrollmentRequest: any = {
    sessionId,
    paymentInstrumentId,
    threeDS: {
      consumerAuthenticationInformation: {
        acsWindowSize: this.settings.customer.acsWindowSize,
      },
    },
  };

  // Create a new payment validation frames action
  const action = framesSDK.createAction(actionType, enrollmentRequest) as ValidatePayment;

  // Start the action, creating a new JWT and initialising cardinal
  await action.start();

  // Set the placeholder for the challenge IFrame to be injected
  action.createFramesControl('3DSValidation', 'overlay');

  const elementHandle = document.getElementById('overlay') as HTMLElement;

  const renderEventListener = () => {
    this.showSpinner = false;
    this.show3DS = true;
  };

  const closeEventListener = () => {
    this.showSpinner = true;
    this.show3DS = false;
  };

  // Add the event listeners for OnRender and OnClose for the 3DS challenge response
  elementHandle.addEventListener(FRAMES.FramesCardinalEventType.OnRender, renderEventListener);
  elementHandle.addEventListener(FRAMES.FramesCardinalEventType.OnClose, closeEventListener);

  // Check card enrolment, allowing cardinal show issuer challenge
  const authorizationResponse = await action.complete();

  // Romove the event listeners for OnRender and OnClose for the 3DS challenge response
  elementHandle.removeEventListener(FRAMES.FramesCardinalEventType.OnRender, renderEventListener);
  elementHandle.removeEventListener(FRAMES.FramesCardinalEventType.OnClose, closeEventListener);

  // 3DS check complete, use returned information to provide a challenge response within the payment endpoint
  console.log(`3DS authorization complete: ${JSON.stringify(authorizationResponse)}`);

  if (actionType === FRAMES.ActionTypes.ValidatePayment) {
    paymentAuthentication = authorizationResponse;
  } else if (actionType === FRAMES.ActionTypes.ValidateCard) {
    cardValidation = authorizationResponse;
  }

  return authorizationResponse;
}

where:

4.2 Show the 3DS Card Capture challenge response modal

When action.complete is called and the renderEventListener is fired, causing:

  • the showSpinner to be set to false, causing this spinner to be hidden and
  • the show3DS variable will be set to true, causing the Challenge Response Modal to be displayed like the one shown below:
215

Sample 3DS2 OTP screen for card capture (hence the $0.00)

Once the challenge is validated (in the above example this means the one time password that has been texted to the customer is entered and submitted) , the closeEventListener is fired, setting the show3DS variable to false hiding the challenge response iFrame.

Additional Information

  • A set of 3DS specific test cards are available to validate the full range of results available from issuers
  • The list of 3DS error codes are listed here
  • 3DS information (e.g. frame sizing) is included on our FAQs