Add 3DS Challenge - Angular
1 //Set Up the 3DS Service - 3ds.service.tsx 2 import { Injectable } from '@angular/core'; 3 import axios from 'axios'; 4 5 export interface ChallengeData { 6 transactionId: string; 7 url: string; 8 creq: string; 9 } 10 11 @Injectable({ 12 providedIn: 'root', 13 }) 14 export class ThreeDsService { 15 private apiUrl = 'https://api-sandbox.coinflow.cash/api/checkout/card/YOUR_MERCHANT_ID'; 16 17 constructor() {} 18 19 // Get 3DS parameters 20 get3DSParams() { 21 return { 22 colorDepth: window.screen.colorDepth, 23 screenHeight: window.screen.height, 24 screenWidth: window.screen.width, 25 timeZone: -new Date().getTimezoneOffset(), 26 }; 27 } 28 29 // Initiates checkout and returns challenge data if 3DS is required 30 initiateCheckout() { 31 const data = { 32 subtotal: { cents: 100 }, 33 authentication3DS: this.get3DSParams(), 34 card: { 35 cardToken: '230377LB2YJJ0408', 36 expYear: '29', 37 expMonth: '10', 38 email: 'dwaynejohnson@therock.com', 39 firstName: 'Dwayne', 40 lastName: 'Johnson', 41 address1: '201 E Randolph St', 42 city: 'Chicago', 43 zip: '60601', 44 state: 'IL', 45 country: 'US', 46 }, 47 saveCard: true, 48 }; 49 50 return axios.post(this.apiUrl, data, { 51 headers: { 52 accept: 'application/json', 53 'content-type': 'application/json', 54 'x-coinflow-auth-session-key': 'PAYER_SESSION_KEY' 55 // 'x-coinflow-auth-blockchain': 'PAYER_BLOCKCHAIN', 56 // 'x-coinflow-auth-wallet': 'PAYER_WALLET_ADDRESS', 57 }, 58 }); 59 } 60 61 // Completes checkout after 3DS challenge 62 completeCheckout(transactionId: string) { 63 const data = { 64 subtotal: { cents: 100 }, 65 authentication3DS: { transactionId }, 66 card: { 67 cardToken: '230377LB2YJJ0408', 68 expYear: '29', 69 expMonth: '10', 70 email: 'dwaynejohnson@therock.com', 71 firstName: 'Dwayne', 72 lastName: 'Johnson', 73 address1: '201 E Randolph St', 74 city: 'Chicago', 75 zip: '60601', 76 state: 'IL', 77 country: 'US', 78 }, 79 saveCard: true, 80 }; 81 82 return axios.post(this.apiUrl, data, { 83 headers: { 84 accept: 'application/json', 85 'content-type': 'application/json', 86 'x-coinflow-auth-session-key': 'PAYER_SESSION_KEY' 87 // 'x-coinflow-auth-blockchain': 'PAYER_BLOCKCHAIN', 88 // 'x-coinflow-auth-wallet': 'PAYER_WALLET_ADDRESS', 89 }, 90 }); 91 } 92 } 93 94 //Challenge Modal Component - challenge-modal.component.tsx 95 import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; 96 import { CommonModule } from '@angular/common'; 97 import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; 98 99 @Component({ 100 selector: 'app-challenge-modal', 101 standalone: true, 102 imports: [CommonModule], 103 templateUrl: './challenge-modal.component.html', 104 styleUrls: ['./challenge-modal.component.css'] 105 }) 106 export class ChallengeModalComponent implements OnInit, OnDestroy { 107 @Input() url: string | null = null; 108 @Input() creq: string | null = null; 109 @Input() transactionId: string | null = null; 110 @Output() closeModal = new EventEmitter<void>(); 111 @Output() challengeComplete = new EventEmitter<string>(); 112 113 error: string | null = null; 114 iframeSrc: SafeResourceUrl | null = null; 115 116 constructor(private sanitizer: DomSanitizer) {} 117 118 ngOnInit(): void { 119 if (!this.url || !this.creq || !this.transactionId) { 120 this.error = 'Missing data for challenge modal!'; 121 return; 122 } 123 124 // Sanitize the iframe url 125 this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.getIframeSrc()); 126 127 const handleMessage = (event: MessageEvent<string>) => { 128 if (event.data === 'challenge_success') { 129 this.challengeComplete.emit(this.transactionId!); 130 } 131 }; 132 133 window.addEventListener('message', handleMessage); 134 this.ngOnDestroy = () => { 135 window.removeEventListener('message', handleMessage); 136 }; 137 } 138 139 ngOnDestroy(): void {} 140 141 onCloseModal() { 142 this.closeModal.emit(); 143 } 144 145 getIframeSrc(): string { 146 const htmlContent = ` 147 <html> 148 <body onload="document.challenge.submit()"> 149 <form method="post" name="challenge" action="${this.url}"> 150 <input type="hidden" name="creq" value="${this.creq}" /> 151 </form> 152 </body> 153 </html> 154 `; 155 const encodedHtml = encodeURIComponent(htmlContent); 156 return 'data:text/html;charset=utf-8,' + encodedHtml; 157 } 158 } 159 160 161 //Challenge Modal Template - challenge-modal.component.html 162 <div class="challenge-modal"> 163 <iframe 164 *ngIf="iframeSrc" 165 [src]="iframeSrc" 166 style="width: 100%; height: 100vh; border: none;" 167 ></iframe> 168 <button (click)="closeModal.emit()">Close</button> 169 </div> 170 171 172 //Implement on Main App Component - app.component.tsx 173 import { Component, OnInit } from '@angular/core'; 174 import { ThreeDsService } from './services/3ds.service'; 175 import { ChallengeModalComponent } from './components/challenge-modal/challenge-modal.component'; 176 import { CommonModule } from '@angular/common'; 177 178 @Component({ 179 selector: 'app-root', 180 standalone: true, 181 imports: [CommonModule, ChallengeModalComponent], 182 templateUrl: './app.component.html', 183 styleUrls: ['./app.component.css'] 184 }) 185 export class AppComponent implements OnInit { 186 challengeData = { 187 url: '', 188 creq: '', 189 transactionId: '' 190 }; 191 showChallengeModal = false; 192 193 constructor(private threeDsService: ThreeDsService) {} 194 195 ngOnInit(): void { 196 this.initiateCheckout(); 197 } 198 199 initiateCheckout() { 200 this.threeDsService.initiateCheckout().then((response: any) => { 201 if (response && response.data) { 202 console.log('Checkout success!', response.data); 203 } 204 }).catch((error) => { 205 console.error('Error during checkout initiation:', error); 206 // handles 3ds challenge requirement 207 if (error.response && error.response.status === 412) { 208 const { transactionId, creq, url } = error.response.data; 209 this.challengeData = { transactionId, creq, url }; 210 this.showChallengeModal = true; 211 } else { 212 console.error('Error', error); 213 } 214 }); 215 } 216 217 handleChallengeComplete(transactionId: string) { 218 console.log('challenge transaction id', transactionId); 219 // completet checkout w/ transaction id after challenge is complete 220 this.threeDsService.completeCheckout(transactionId) 221 .then(response => { 222 console.log('success', response); 223 this.challengeData = { url: '', creq: '', transactionId: '' }; 224 this.showChallengeModal = false; 225 }) 226 .catch(error => { 227 console.error('error', error); 228 }); 229 } 230 231 closeModal() { 232 this.showChallengeModal = false; 233 } 234 } 235 236 237 //Add the App HTML Template - app.component.html 238 <app-challenge-modal 239 *ngIf="showChallengeModal" 240 [url]="challengeData.url" 241 [creq]="challengeData.creq" 242 [transactionId]="challengeData.transactionId" 243 (challengeComplete)="handleChallengeComplete($event)" 244 (closeModal)="closeModal()"> 245 </app-challenge-modal>
1 {"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.

