-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d78a220
commit 0fa9098
Showing
5 changed files
with
172 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters