Add 3DS Challenge - Angular
Add 3DS Challenge - Angular
//Set Up the 3DS Service - 3ds.service.tsximport { 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 parametersget3DSParams() {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 requiredinitiateCheckout() {const data = {subtotal: { cents: 198 }, // Subtotal ending in 98 cents will force a 3ds challenge in sandboxauthentication3DS: this.get3DSParams(),card: {cardToken: 'YOUR_CARD_TOKEN', // Use any valid tokenized cardexpYear: '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 challengecompleteCheckout(transactionId: string) {const data = {subtotal: { cents: 198 }, // Must match the subtotal from the initial requestauthentication3DS: { transactionId },card: {cardToken: 'YOUR_CARD_TOKEN', // Use any valid tokenized cardexpYear: '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.tsximport { 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 urlthis.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.tsximport { 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 requirementif (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 completethis.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>
{"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.

