Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

iFramed credit card processing for cloud

Describes how to configure credit card processing through TokenEx for Commerce (SaaS).

Optimizely uses TokenEx (https://tokenex.com/) as a common payment gateway provider for the Cloud offering for a variety of reasons including improved security, and simpler PCI compliance. To use this provider, all Cloud customers must implement an iframe to capture the credit card number and CVV. Our implementation also has some credit card formatting as the card number is entered.

In _MainLayout we include the reference to the TokenEx JavaScript library (Test:https://test-htp.tokenex.com/Iframe/Iframe-v3.min.js , Prod:https://htp.tokenex.com/Iframe/Iframe-v3.min.js).

To support these updates in our Cloud environment, you must make the changes indicated below. The setting for TokenEx is under Administration > System > Settings, but this is controlled by the appSettings.config file.

📘

Note

The Commerce (SaaS) Mobile App also supports credit card payments via TokenEx.

ReviewAndPay Widget

If you do not have a customized ReviewAndPay widget, you can merge the changes from our updated Responsive theme ReviewAndPay widget. If you do have a customized version of this widget, you must update the Credit Card Number and Security Code inputs like the code below. The TokenEx Javascript functions will replace the

with id "tokenExCardNumber" with the card number iframe and the
with id "pptokenExSecurityCode" with the security code iframe.

Code Sample: TokenEx IFrame

<div class="card-num">
	<label for="cardNumber">[% translate 'Card Number' %]</label>
        <span ng-if="vm.useTokenExGateway">
        	<div id="tokenExCardNumber" style="width: 100%; height: 37.8px;"></div>
            	<label ng-if="vm.isInvalidCardNumber" class="error">[% translate 'Credit card number is invalid.' %]</label>
        </span>
        <span ng-if="!vm.useTokenExGateway">
        	<input id="cardNumber" name="cardNumber" maxlength="16" type="text" style="display: block;"
           	 	data-rule-required="true" data-msg-required="[% translate 'Credit card number is required.' %]"
           	 	data-rule-creditcard="true" data-msg-creditcard="[% translate 'Credit card number is invalid.' %]"
           	 	ng-model="vm.cart.paymentOptions.creditCard.cardNumber">
            	<span class="field-validation-valid" data-valmsg-for="cardNumber" data-valmsg-replace="true"></span>
        </span>
</div>
<div class="security-code">
	<label>[% translate 'Security Code' %] <a href="javascript:void(0);" data-reveal-id="whatsThisPopup">[% translate "What's This?" %]</a></label>
        <span ng-if="vm.useTokenExGateway">
 <input type="text" disabled ng-show="!vm.ppTokenExIframeIsLoaded" />
 <div id="ppTokenExSecurityCode" class="tokenex-iframe-block" ng-show="vm.ppTokenExIframeIsLoaded"></div>
 <label ng-if="vm.ppIsInvalidSecurityCode" class="tokenex-error error">[% translate 'Security code is invalid.' %]</label>
</span>
<span ng-if="!vm.useTokenExGateway">
 <input id="ppSecurityCode" name="ppSecurityCode" type="text" minlength="3" maxlength="4"
 data-rule-required="true" data-msg-required="[% translate 'Security code is required.' %]"
 data-rule-digits="true" data-msg-digits="[% translate 'Security code is invalid.' %]"
 ng-model="vm.cart.paymentOptions.creditCard.securityCode">
</span>
        <div id="whatsThisPopup" class="checkout-review-pay reveal-modal" data-reveal>
        	<div class="modal-wrap">
            		<img src="/Images/Default/security_code_sample.jpg" alt="Security Code Sample"/>
            		<a class="close-reveal-modal">×</a>
           	</div>
        </div>
</div>

insite.review-and-pay.controller.ts

You must merge the changes from our updated Responsive theme /scripts/cart/insite.review-and-pay.controller.ts in to your theme. If you made any changes to this controller by extending it in typescript with your own version of the controller (following the recommended way to extend our controllers), this should be all you need to do. If you actually changed it directly or replaced it without extending from our controller, you must manually merge the changes into your version. The functions to set up the TokenEx iframe are in this controller:

Code Sample: insite.review-and-pay.controller.ts

setUpTokenExGateway(): void {
	if (!this.useTokenExGateway) {
            return;
       }
             
       this.settingsService.getTokenExConfig().then(
           (tokenExDto: TokenExDto) => {
            	this.getTokenExConfigCompleted(tokenExDto);
           },
           (error: any) => {
            	this.getTokenExConfigFailed(error);
           });
}
             
protected tokenizeCardInfoIfNeeded(submitSuccessUri: string) {
    this.submitSuccessUri = submitSuccessUri;

    if (this.useTokenExGateway && this.cart.showCreditCard && !this.cart.paymentOptions.isPayPal) {
        if (this.cart.paymentMethod.isCreditCard) {
            this.tokenExIframe.tokenize();
            return;
        }

        if (this.cart.paymentMethod.isPaymentProfile) {
            this.ppTokenExIframe.tokenize();
            return;
        }
    }

    this.submitCart();
}
             
       this.tokenExIframe.on("validate", (data) => {
        	this.$scope.$apply(() => {
            	     if (data.isValid) {
            		 this.isInvalidCardNumber = false;
            	     } else {
            		 if (this.submitting) {
            		     this.isInvalidCardNumber = true;
            		 } else if (data.validator && data.validator !== "required") {
            		     this.isInvalidCardNumber = true;
            		 }
                    }
             
            	     if (data.isCvvValid) {
            		 this.isInvalidSecurityCode = false;
            	     } else {
                        if (this.submitting) {
            		     this.isInvalidSecurityCode = true;
            		 } else if (data.cvvValidator && data.cvvValidator !== "required") {
            		     this.isInvalidSecurityCode = true;
            		 }
           	     }
             
            	     if (this.submitting && (this.isInvalidCardNumber || this.isInvalidSecurityCode)) {
            		 this.submitting = false;
            		 this.spinnerService.hide();
           	     }
            	  });
             });
             
             this.tokenExIframe.on("error", (data) => {
            	  this.$scope.$apply(() => {
            		// if there was some sort of unknown error from tokenex tokenization (the example they gave was authorization timing out)
            		// try to completely re-initialize the tokenex iframe
             	 	this.setUpTokenExGateway();
            	   });
             });
          }
             
 protected getTokenExIframeConfig(tokenExDto: TokenExDto): any {
 	return {
            origin: tokenExDto.origin,
            timestamp: tokenExDto.timestamp,
            tokenExID: tokenExDto.tokenExId,
            tokenScheme: tokenExDto.tokenScheme,
            authenticationKey: tokenExDto.authenticationKey,
            styles: {
            	base: "font-family: Arial, sans-serif;padding: 0 8px;border: 1px solid rgba(0, 0, 0, 0.2);margin: 0;width: 100%;font-size: 13px;line-height: 30px;height: 2.7em;box-sizing: border-box;-moz-box-sizing: border-box;",
            	focus: "box-shadow: 0 0 6px 0 rgba(0, 132, 255, 0.5);border: 1px solid rgba(0, 132, 255, 0.5);outline: 0;",
            	error: "box-shadow: 0 0 6px 0 rgba(224, 57, 57, 0.5);border: 1px solid rgba(224, 57, 57, 0.5);",
            	cvv: {
            		base: "font-family: Arial, sans-serif;padding: 0 8px;border: 1px solid rgba(0, 0, 0, 0.2);margin: 0;width: 100%;font-size: 13px;line-height: 30px;height: 2.7em;box-sizing: border-box;-moz-box-sizing: border-box;",
            		focus: "box-shadow: 0 0 6px 0 rgba(0, 132, 255, 0.5);border: 1px solid rgba(0, 132, 255, 0.5);outline: 0;",
            		error: "box-shadow: 0 0 6px 0 rgba(224, 57, 57, 0.5);border: 1px solid rgba(224, 57, 57, 0.5);",
            	}
            },
            pci: true,
            enableValidateOnBlur: true,
            inputType: "text",
            enablePrettyFormat: true,
            cvv: true,
            cvvContainerID: "tokenExSecurityCode",
            cvvInputType: "Number"
        };
}
setUpPPTokenExGateway(): void {
    if (!this.useTokenExGateway) {
        return;
    }

    this.ppTokenExIframeIsLoaded = false;

    this.settingsService.getTokenExConfig(this.cart.paymentMethod.name).then(
        (tokenExDto: TokenExDto) => {
            this.getTokenExConfigForCVVOnlyCompleted(tokenExDto);
        },
        (error: any) => {
            this.getTokenExConfigForCVVOnlyFailed(error);
        });
}

protected getTokenExConfigForCVVOnlyCompleted(tokenExDto: TokenExDto) {
    this.setUpPPTokenExIframe(tokenExDto);
}

protected setUpPPTokenExIframe(tokenExDto: TokenExDto) {
    if (!this.useTokenExGateway) {
        return;
    }

    if (this.ppTokenExIframe) {
        this.ppTokenExIframe.remove();
    }

    this.ppTokenExIframe = new TokenEx.Iframe("ppTokenExSecurityCode", this.getPPTokenExIframeConfig(tokenExDto));

    this.ppTokenExIframe.load();

    this.ppTokenExIframe.on("load", () => {
        this.$scope.$apply(() => {
            this.ppTokenExIframeIsLoaded = true;
            this.ppIsInvalidSecurityCode = false;
        });
    });

    this.ppTokenExIframe.on("tokenize", () => {
        this.$scope.$apply(() => {
            this.submitCart();
        });
    });

    this.ppTokenExIframe.on("validate", (data) => {
        this.$scope.$apply(() => {
            if (data.isValid) {
                this.ppIsInvalidSecurityCode = false;
            } else {
                if (this.submitting) {
                    this.ppIsInvalidSecurityCode = true;
                } else if (data.validator && data.validator !== "required") {
                    this.ppIsInvalidSecurityCode = true;
                }
            }

            if (this.submitting && this.ppIsInvalidSecurityCode) {
                this.submitting = false;
                this.spinnerService.hide();
            }
        });
    });

    this.ppTokenExIframe.on("error", () => {
        this.$scope.$apply(() => {
            // if there was some sort of unknown error from tokenex tokenization (the example they gave was authorization timing out)
            // try to completely re-initialize the tokenex iframe
            this.setUpPPTokenExGateway();
        });
    });
}

protected getTokenExConfigForCVVOnlyFailed(error: any): void {
}

protected getPPTokenExIframeConfig(tokenExDto: TokenExDto): any {
    return {
        origin: tokenExDto.origin,
        timestamp: tokenExDto.timestamp,
        tokenExID: tokenExDto.tokenExId,
        token: this.cart.paymentMethod.name,
        tokenScheme: this.cart.paymentMethod.tokenScheme,
        authenticationKey: tokenExDto.authenticationKey,
        styles: {
            base: "font-family: Arial, sans-serif;padding: 0 8px;border: 1px solid rgba(0, 0, 0, 0.2);margin: 0;width: 100%;font-size: 14px;line-height: 30px;height: 2.7em;box-sizing: border-box;-moz-box-sizing: border-box;",
            focus: "box-shadow: 0 0 6px 0 rgba(0, 132, 255, 0.5);border: 1px solid rgba(0, 132, 255, 0.5);outline: 0;",
            error: "box-shadow: 0 0 6px 0 rgba(224, 57, 57, 0.5);border: 1px solid rgba(224, 57, 57, 0.5);",
            cvv: {
                base: "font-family: Arial, sans-serif;padding: 0 8px;border: 1px solid rgba(0, 0, 0, 0.2);margin: 0;width: 100%;font-size: 14px;line-height: 30px;height: 2.7em;box-sizing: border-box;-moz-box-sizing: border-box;",
                focus: "box-shadow: 0 0 6px 0 rgba(0, 132, 255, 0.5);border: 1px solid rgba(0, 132, 255, 0.5);outline: 0;",
                error: "box-shadow: 0 0 6px 0 rgba(224, 57, 57, 0.5);border: 1px solid rgba(224, 57, 57, 0.5);",
            }
        },
        enableValidateOnBlur: true,
        cvv: true,
        cvvOnly: true,
        inputType: "text",
        cardType: this.getValidTokenExCardType(this.cart.paymentMethod.cardType)
    };
}

private getValidTokenExCardType(cardType: string): string {
    cardType = cardType.toLowerCase();
    if (cardType === "visa") {
        return cardType;
    } else if (cardType === "mastercard") {
        return "masterCard";
    } else if (cardType === "discover") {
        return cardType;
    } else if (cardType.indexOf("american") > -1) {
        return "americanExpress";
    } else {
        return cardType;
    }
}

First we call this.settingsService.getTokenExConfig(), which calls /api/v1/tokenexconfig to get the information to set up this TokenEx iframe instance. You must call this, as it sets up a unique authentication key using the timestamp and api key (that is not exposed), which must match and is used later with the transparent gateway calls. Next, the call then sets up the TokenEx iframes in the getTokenExConfigCompleted when it creates a new instance of the TokenEx.Iframe object.

The first parameter to the constructor is the id of the

for the credit card iframe and the second is the configuration of that iframe. This is where you can control the styling of the iframe and note that the CVV iframe is set up in the configuration of the card number iframe and tied directly to that card number iframe.

Then this.tokenExIframe.load() is called, which loads the iframes into the page and event handlers are set up to listen to events issued from the iframes. The validate event is fired when the iframes are exited (on blur) and the tokenize event is fired when the submit button is clicked. We set the card number equal to the token we get back and the security code to just CVV, as the CVV token is already tied to the token for that card number and not needed or returned. See TokenEx iframe.

TokenEx transparent gateway

When the iframe is used client side, the submit order sends the TokenEx token for the credit card number and CVV for the security code. What then happens server side is that we enclose these values with three curly braces like {{{token}}} and {{{CVV}}} and for whichever payment gateway is used, we replace the URL for that request to the TokenEx transparent gateway URL and then we set three headers on the request:

  • TX_URL – gets set to the actual payment gateway url that we replaced
  • TX_TokenExID – gets set to our TokenEx Id
  • TX_APIKey – gets set to our TokenEx API Key

The request goes through the TokenEx transparent gateway and the values in the request {{{token}}} and {{{CVV}}} get replaced with the actual card number and security code. Then the request is forwarded on to the actual payment gateway and we get the same response back from the payment gateway. See TokenEx iframe.

Saved credit cards

When customers save a credit card, the system uses the same tokenization method of storing the token in the UserPaymentProfile table. When referencing a saved card, the system references information from the UserPaymentProfile table. When using a saved card, customers will need to enter a CVV number. A TokenEx iFrame is used for this, and calls the CVV-only mode configuration.

📘

Note

If the site is processing credit cards, Optimizely requires you to enable TokenEx.