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: 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
95import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
96import { CommonModule } from '@angular/common';
97import { 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})
106export 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
173import { Component, OnInit } from '@angular/core';
174import { ThreeDsService } from './services/3ds.service';
175import { ChallengeModalComponent } from './components/challenge-modal/challenge-modal.component';
176import { 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})
185export 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>
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.