Skip to content

Commit

Permalink
fix comments
Browse files Browse the repository at this point in the history
  • Loading branch information
zeyarpaing committed Nov 20, 2024
1 parent d78a220 commit 0fa9098
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 169 deletions.
88 changes: 88 additions & 0 deletions packages/vanilla/3ds-elements/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Ping3DSStatusResponse, ThreeDSStatus } from '@getopenpay/utils';
import { pingCdeFor3dsStatus } from '../utils/connection';
import { createAndOpenFrame } from './frame';

export interface PopupElements {
host: HTMLElement;
iframe: HTMLIFrameElement;
cancelButton: HTMLButtonElement;
}

export function startPolling(
iframe: HTMLIFrameElement,
onSuccess: (status: Ping3DSStatusResponse['status']) => void,
childOrigin: string
): NodeJS.Timeout {
const handlePolling = async () => {
try {
console.log('🔄 Polling CDE connection...');
const status = await pingCdeFor3dsStatus(iframe, childOrigin);
if (status) {
console.log('🟢 CDE connection successful! Stopping polling...');
console.log('➡️ Got status:', status);
clearInterval(pollingInterval);
onSuccess(status);
}
} catch (error) {
// Connection failed, continue polling
}
};
handlePolling();
const pollingInterval = setInterval(handlePolling, 1000); // Poll every second
return pollingInterval;
}

export function handleEvents({
elements,
pollingInterval,
resolve,
}: {
elements: PopupElements;
pollingInterval: NodeJS.Timeout;
resolve: (value: Ping3DSStatusResponse['status']) => void;
}) {
const handleCancel = () => {
clearInterval(pollingInterval);
elements.host.remove();
resolve(ThreeDSStatus.CANCELLED);
};

elements.cancelButton.addEventListener('click', handleCancel);

const cleanupEventListeners = () => {
elements.cancelButton.removeEventListener('click', handleCancel);
};

return cleanupEventListeners;
}

/**
* @returns `Promise<'success' | 'failure' | 'cancelled'>`
*/
export async function start3dsVerification({
url,
baseUrl,
}: {
url: string;
baseUrl: string;
}): Promise<Ping3DSStatusResponse['status']> {
const elements = createAndOpenFrame(url);

return new Promise((resolve) => {
const onSuccess = (status: Ping3DSStatusResponse['status']) => {
setTimeout(() => {
elements.host.remove();
}, 1000); // To show the success/failure message for a second
resolve(status);
cleanupEventListeners?.();
};

const pollingInterval = startPolling(elements.iframe, onSuccess, new URL(baseUrl).origin);

const cleanupEventListeners = handleEvents({
elements,
pollingInterval,
resolve,
});
});
}
187 changes: 22 additions & 165 deletions packages/vanilla/3ds-elements/frame.ts
Original file line number Diff line number Diff line change
@@ -1,169 +1,26 @@
import { Ping3DSStatusResponse, ThreeDSStatus } from '@getopenpay/utils';
import { pingCdeFor3dsStatus } from '../utils/connection';
export const SIMULATE_3DS_URL = 'http://localhost:3033/simulate-3ds.html';

function startPolling(
iframe: HTMLIFrameElement,
onSuccess: (status: Ping3DSStatusResponse['status']) => void,
childOrigin: string
): NodeJS.Timeout {
const handlePolling = async () => {
try {
console.log('🔄 Polling CDE connection...');
const status = await pingCdeFor3dsStatus(iframe, childOrigin);
if (status) {
console.log('🟢 CDE connection successful! Stopping polling...');
console.log('➡️ Got status:', status);
clearInterval(pollingInterval);
onSuccess(status);
}
} catch (error) {
// console.error('🔴 CDE connection failed, continuing to poll...');
// Connection failed, continue polling
}
};
handlePolling();
const pollingInterval = setInterval(handlePolling, 1000); // Poll every second
return pollingInterval;
}
import styles from './style.css?inline';

const createStyles = () => {
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
export const SIMULATE_3DS_URL = 'http://localhost:3033/simulate-3ds.html';

.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2147483648 !important;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
}
.container {
width: clamp(28rem, 35%, 30rem);
height: clamp(20rem, 80%, 46rem);
background-color: white;
position: relative;
animation: fadeIn 0.3s ease-out;
}
.frame {
width: 100%;
height: 100%;
border: none;
}
.cancel-button {
position: absolute;
background: transparent;
border: none;
font-size: 1rem;
color: #fff;
cursor: pointer;
top: -1.5rem;
padding: 0;
right: 0;
}
export const createAndOpenFrame = (url: string) => {
const template = document.createElement('template');
template.innerHTML = `
<style>
${styles}
</style>
<div class="overlay">
<div class="container">
<button class="cancel-button">Cancel</button>
<iframe src="${url}" class="frame" id="three-ds-iframe" title="3D Secure verification" allow="payment"></iframe>
</div>
</div>
`;
return style;
const host = document.createElement('div');
const shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.content.cloneNode(true));
const iframe = shadowRoot.querySelector('#three-ds-iframe') as HTMLIFrameElement;
const cancelButton = shadowRoot.querySelector('.cancel-button') as HTMLButtonElement;
document.body.appendChild(host);

return { host, iframe, cancelButton };
};

interface DOMElements {
shadowHost: HTMLDivElement;
shadowRoot: ShadowRoot;
overlay: HTMLDivElement;
container: HTMLDivElement;
frame: HTMLIFrameElement;
cancelButton: HTMLButtonElement;
}

const constructPopup = (url: string): DOMElements => {
const shadowHost = document.createElement('div');
// Used shadowRoot to avoid CSS conflicts with the parent
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const overlay = document.createElement('div');
const container = document.createElement('div');
const frame = document.createElement('iframe');
const cancelButton = document.createElement('button');

const style = createStyles();
overlay.className = 'overlay';
container.className = 'container';
frame.className = 'frame';
frame.src = url;
cancelButton.textContent = 'Cancel';
cancelButton.className = 'cancel-button';

shadowRoot.appendChild(style);
container.appendChild(cancelButton);
container.appendChild(frame);
overlay.appendChild(container);
shadowRoot.appendChild(overlay);
document.body.appendChild(shadowHost);

return {
shadowHost,
shadowRoot,
overlay,
container,
frame,
cancelButton,
};
};

/**
* @returns `Promise<'success' | 'failure' | 'cancelled'>`
*/
export async function start3dsVerification({
url,
baseUrl,
}: {
url: string;
baseUrl: string;
}): Promise<Ping3DSStatusResponse['status']> {
const { shadowHost, shadowRoot, overlay, frame, cancelButton } = constructPopup(url);

// Cleanup function that handles all DOM removal
function cleanup() {
if (shadowRoot.contains(overlay)) {
shadowRoot.removeChild(overlay);
}
if (document.body.contains(shadowHost)) {
document.body.removeChild(shadowHost);
}
}

return new Promise((resolve) => {
// Setup connection polling
const pollingInterval = startPolling(
frame,
(status) => {
setTimeout(() => {
cleanup();
}, 1500);
resolve(status);
cleanupEventListeners();
}, // onSuccessCallback
new URL(baseUrl).origin // childOrigin
);

function handleCancel() {
clearInterval(pollingInterval);
cleanup();
resolve(ThreeDSStatus.CANCELLED);
cleanupEventListeners();
}

cancelButton.addEventListener('click', handleCancel);

function cleanupEventListeners() {
cancelButton.removeEventListener('click', handleCancel);
}
});
}
46 changes: 46 additions & 0 deletions packages/vanilla/3ds-elements/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}

.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2147483648 !important;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
}
.container {
width: clamp(28rem, 35%, 30rem);
height: clamp(20rem, 80%, 46rem);
background-color: white;
position: relative;
animation: fadeIn 0.3s ease-out;
}
.frame {
width: 100%;
height: 100%;
border: none;
}
.cancel-button {
position: absolute;
background: transparent;
border: none;
font-size: 1rem;
color: #fff;
cursor: pointer;
top: -1.5rem;
padding: 0;
right: 0;
}
9 changes: 5 additions & 4 deletions packages/vanilla/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
import { OpenPayForm } from './index';
import { ConnectionManager } from './utils/connection';
import { PaymentRequestPaymentMethodEvent } from '@stripe/stripe-js';
import { start3dsVerification, SIMULATE_3DS_URL } from './3ds-elements/frame';
import { SIMULATE_3DS_URL } from './3ds-elements/frame';
import { start3dsVerification } from './3ds-elements/events';

export class OpenPayFormEventHandler {
formInstance: OpenPayForm;
Expand Down Expand Up @@ -154,6 +155,8 @@ export class OpenPayFormEventHandler {
}

async handleLoadedEvent(source: MessageEventSource, elementId: string, payload: LoadedEventPayload) {
const status = await start3dsVerification({ url: SIMULATE_3DS_URL, baseUrl: this.config.baseUrl! });
console.log('🔐 3DS status:', status);
this.eventTargets[elementId] = source;
if (!this.formInstance.sessionId) {
this.formInstance.sessionId = payload.sessionId;
Expand Down Expand Up @@ -298,9 +301,7 @@ export class OpenPayFormEventHandler {
const threeDSUrl = payload.headers?.['x-3ds-auth-url'] ?? SIMULATE_3DS_URL;
// This will open a popup and process the 3DS flow
// will return a status `success` | `failure` | `cancelled` which we can continue with
const status = await start3dsVerification({ url: threeDSUrl, baseUrl: this.config.baseUrl! }).catch((e) => {
console.error('🔴 Error 3DS flow:', e);
});
const status = await start3dsVerification({ url: threeDSUrl, baseUrl: this.config.baseUrl! });
console.log('🔐 3DS status:', status);
// TODO: continue with status

Expand Down
11 changes: 11 additions & 0 deletions packages/vanilla/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export default defineConfig(({ mode }) => {
rollupTypes: true,
tsconfigPath: resolve(__dirname, 'tsconfig.build.json'),
}),
{
name: 'css-inline',
transform(code, id) {
if (id.endsWith('.css?inline')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
};
}
},
}, // To inline CSS into the bundle
],
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
Expand Down

0 comments on commit 0fa9098

Please sign in to comment.