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

