iFramed credit card processing for SDK
You may use TokenEx as a common payment gateway provider for your Optimizely Configured Commerce SDK offering for a variety of reasons, including improved security and simpler PCI compliance. To use this provider, implement an iframe to capture the credit card number and CVV. This implementation also has some credit card formatting as the card number is entered.
Note
SDK/Self-Managed customers are responsible for their own PCI-DSS compliance and can choose to open their own TokenEx account.
_MainLayout references 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, make the changes indicated below. The setting for TokenEx is under Administration > Settings but is controlled by the appSettings.config
file.
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 <div> with id "tokenExCardNumber" with the card number iframe and the <div> 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 <div> 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.
For more information on setting up the TokenEx iframe, see Creating the 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.
For information on the TokenEx transparent gateway see Welcome to TokenEx.
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.
Updated 9 months ago