Add 3DS Challenge - Angular
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 // '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: 198 }, // Must match the subtotal from the initial request 65 authentication3DS: { transactionId }, 66 card: { 67 cardToken: 'YOUR_CARD_TOKEN', // Use any valid tokenized card 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 a 3DSService to make API calls for initiating and completing checkout sessions. This service handles requests to the checkout api and processes 3DS authentication.
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.
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.
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.
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.
