Add 3DS Challenge - Angular

3DS Challenge
//Set Up the 3DS Service - 3ds.service.tsx
import { Injectable } from '@angular/core';
import axios from 'axios';
export interface ChallengeData {
transactionId: string;
url: string;
creq: string;
}
@Injectable({
providedIn: 'root',
})
export class ThreeDsService {
private apiUrl = 'https://api-sandbox.coinflow.cash/api/checkout/card/YOUR_MERCHANT_ID';
constructor() {}
// Get 3DS parameters
get3DSParams() {
return {
colorDepth: window.screen.colorDepth,
screenHeight: window.screen.height,
screenWidth: window.screen.width,
timeZone: -new Date().getTimezoneOffset(),
};
}
// Initiates checkout and returns challenge data if 3DS is required
initiateCheckout() {
const data = {
subtotal: { cents: 198 }, // Subtotal ending in 98 cents will force a 3ds challenge in sandbox
authentication3DS: this.get3DSParams(),
card: {
cardToken: 'YOUR_CARD_TOKEN', // Use any valid tokenized card
expYear: '29',
expMonth: '10',
email: 'dwaynejohnson@therock.com',
firstName: 'Dwayne',
lastName: 'Johnson',
address1: '201 E Randolph St',
city: 'Chicago',
zip: '60601',
state: 'IL',
country: 'US',
},
saveCard: true,
};
return axios.post(this.apiUrl, data, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'x-coinflow-auth-session-key': 'PAYER_SESSION_KEY'
},
});
}
// Completes checkout after 3DS challenge
completeCheckout(transactionId: string) {
const data = {
subtotal: { cents: 198 }, // Must match the subtotal from the initial request
authentication3DS: { transactionId },
card: {
cardToken: 'YOUR_CARD_TOKEN', // Use any valid tokenized card
expYear: '29',
expMonth: '10',
email: 'dwaynejohnson@therock.com',
firstName: 'Dwayne',
lastName: 'Johnson',
address1: '201 E Randolph St',
city: 'Chicago',
zip: '60601',
state: 'IL',
country: 'US',
},
saveCard: true,
};
return axios.post(this.apiUrl, data, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'x-coinflow-auth-session-key': 'PAYER_SESSION_KEY'
},
});
}
}
//Challenge Modal Component - challenge-modal.component.tsx
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-challenge-modal',
standalone: true,
imports: [CommonModule],
templateUrl: './challenge-modal.component.html',
styleUrls: ['./challenge-modal.component.css']
})
export class ChallengeModalComponent implements OnInit, OnDestroy {
@Input() url: string | null = null;
@Input() creq: string | null = null;
@Input() transactionId: string | null = null;
@Output() closeModal = new EventEmitter<void>();
@Output() challengeComplete = new EventEmitter<string>();
error: string | null = null;
iframeSrc: SafeResourceUrl | null = null;
constructor(private sanitizer: DomSanitizer) {}
ngOnInit(): void {
if (!this.url || !this.creq || !this.transactionId) {
this.error = 'Missing data for challenge modal!';
return;
}
// Sanitize the iframe url
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.getIframeSrc());
const handleMessage = (event: MessageEvent<string>) => {
if (event.data === 'challenge_success') {
this.challengeComplete.emit(this.transactionId!);
}
};
window.addEventListener('message', handleMessage);
this.ngOnDestroy = () => {
window.removeEventListener('message', handleMessage);
};
}
ngOnDestroy(): void {}
onCloseModal() {
this.closeModal.emit();
}
getIframeSrc(): string {
const htmlContent = `
<html>
<body onload="document.challenge.submit()">
<form method="post" name="challenge" action="${this.url}">
<input type="hidden" name="creq" value="${this.creq}" />
</form>
</body>
</html>
`;
const encodedHtml = encodeURIComponent(htmlContent);
return 'data:text/html;charset=utf-8,' + encodedHtml;
}
}
//Challenge Modal Template - challenge-modal.component.html
<div class="challenge-modal">
<iframe
*ngIf="iframeSrc"
[src]="iframeSrc"
style="width: 100%; height: 100vh; border: none;"
></iframe>
<button (click)="closeModal.emit()">Close</button>
</div>
//Implement on Main App Component - app.component.tsx
import { Component, OnInit } from '@angular/core';
import { ThreeDsService } from './services/3ds.service';
import { ChallengeModalComponent } from './components/challenge-modal/challenge-modal.component';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ChallengeModalComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
challengeData = {
url: '',
creq: '',
transactionId: ''
};
showChallengeModal = false;
constructor(private threeDsService: ThreeDsService) {}
ngOnInit(): void {
this.initiateCheckout();
}
initiateCheckout() {
this.threeDsService.initiateCheckout().then((response: any) => {
if (response && response.data) {
console.log('Checkout success!', response.data);
}
}).catch((error) => {
console.error('Error during checkout initiation:', error);
// handles 3ds challenge requirement
if (error.response && error.response.status === 412) {
const { transactionId, creq, url } = error.response.data;
this.challengeData = { transactionId, creq, url };
this.showChallengeModal = true;
} else {
console.error('Error', error);
}
});
}
handleChallengeComplete(transactionId: string) {
console.log('challenge transaction id', transactionId);
// completet checkout w/ transaction id after challenge is complete
this.threeDsService.completeCheckout(transactionId)
.then(response => {
console.log('success', response);
this.challengeData = { url: '', creq: '', transactionId: '' };
this.showChallengeModal = false;
})
.catch(error => {
console.error('error', error);
});
}
closeModal() {
this.showChallengeModal = false;
}
}
//Add the App HTML Template - app.component.html
<app-challenge-modal
*ngIf="showChallengeModal"
[url]="challengeData.url"
[creq]="challengeData.creq"
[transactionId]="challengeData.transactionId"
(challengeComplete)="handleChallengeComplete($event)"
(closeModal)="closeModal()">
</app-challenge-modal>
Response Example
{"success":true}

Set Up the 3DS Service

Set up a 3DSService to make API calls for initiating and completing checkout sessions. This service handles requests to the checkout api and processes 3DS authentication.

Challenge Modal Component

Create a ChallengeModalComponent that displays the 3DS challenge in an iframe. This will listen for a message that the challenge has completed, then emit an event to proceed with the checkout.

Challenge Modal Template

Build template for the ChallengeModalComponent. This includes the iframe that submits the challenge form and a close button to allow users to exit the challenge.

Implement on Main App Component

Set up the AppComponent to manage the overall 3DS flow. This component triggers the checkout, displays the challenge modal on a 412 response, and completes the checkout once the challenge succeeds.

Add the App HTML Template

Integrate the ChallengeModalComponent in app.component.html. This template conditionally displays the modal when a 3DS challenge is required and passes necessary data using component bindings.