Add 3DS Challenge - Angular

3DS Challenge
1//Set Up the 3DS Service - 3ds.service.tsx
2import { Injectable } from '@angular/core';
3import axios from 'axios';
4
5export interface ChallengeData {
6 transactionId: string;
7 url: string;
8 creq: string;
9}
10
11@Injectable({
12 providedIn: 'root',
13})
14export 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
91import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
92import { CommonModule } from '@angular/common';
93import { 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})
102export 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
169import { Component, OnInit } from '@angular/core';
170import { ThreeDsService } from './services/3ds.service';
171import { ChallengeModalComponent } from './components/challenge-modal/challenge-modal.component';
172import { 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})
181export 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>
Response Example
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.