Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions docs/apm-ui-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,14 +574,6 @@ const apm = client.apm.authorization(container, {
timeout: 900, // Timeout in seconds (15 minutes)
allowCancelation: true // Allow cancellation during confirmation
},

// Redirect step (web only): optional keys match Context.ts FlowData.redirect (see sdks-embedded-components.md)
redirect: {
enableHeadlessMode: false,
// silentFailureView: false,
// showHeadlessLoader: true,
// actionOverlayMountParent: document.getElementById('overlay-root'), // HTMLElement | null
},

// Success screen settings
success: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "processout.js",
"version": "1.8.8",
"version": "1.8.7",
"description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.",
"scripts": {
"build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js",
Expand Down
24 changes: 0 additions & 24 deletions src/apm/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,6 @@ module ProcessOut {
/** Whether user must take action to dismiss success screen (default: false) */
requiresAction: boolean
}

/**
* Client-side redirect behaviour (web APM). Aligns with mobile
* `RedirectConfiguration.enableHeadlessMode`: skip the intermediate
* “Continue to payment” / Pay button and open the PSP flow as soon as the
* redirect step is shown.
*/
redirect?: {
enableHeadlessMode?: boolean
/**
* When headless, emit `failure` for `handleAction` errors but do not render the in-widget
* error screen (mobile-style; merchant handles UX). Popup-blocked fallback UI is unchanged.
* Default false for backward compatibility.
*/
silentFailureView?: boolean
/**
* When headless, show the loader while opening the PSP. Default true; set false for no in-widget chrome.
*/
showHeadlessLoader?: boolean
/**
* Append `ActionHandler` iframe modal / new-window overlay to this element instead of `document.body`.
*/
actionOverlayMountParent?: HTMLElement | null
}
}

export type TokenizationUserData = TokenizationFlowData & FlowData
Expand Down
19 changes: 6 additions & 13 deletions src/apm/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,11 @@ module ProcessOut {
})
}

criticalFailure(
{
title,
code,
message,
}: { message: string, title: string, code?: string, },
options?: { renderErrorView?: boolean },
) {
criticalFailure({
title,
code,
message,
}: { message: string, title: string, code?: string, }) {
ContextImpl.context.events.emit("failure", {
failure: {
code: code || 'processout-js.internal-error',
Expand All @@ -107,11 +104,7 @@ module ProcessOut {
paymentState: this.state
})

if (options && options.renderErrorView === false) {
return
}

this.render(APMViewError, {
ContextImpl.context.page.render(APMViewError, {
title: title || "Unable to connect",
message: message || "An unexpected error occurred. We're working to fix this issue, please check back later or contact support if you need assistance.",
hideRefresh: true
Expand Down
8 changes: 2 additions & 6 deletions src/apm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,13 @@ module ProcessOut {
requiresAction: false,
autoDismissDuration: 3,
manualDismissDuration: 60,
...(data.success || {}),
...data.success,
},
confirmation: {
requiresAction: false,
timeout: MIN_15 / 1000,
allowCancelation: true,
...(data.confirmation || {}),
},
redirect: {
enableHeadlessMode: false,
...(data.redirect || {}),
...data.confirmation,
},
logger: {
error: (options: Omit<Parameters<TelemetryClient['reportError']>[0], 'stack'>) => {
Expand Down
83 changes: 7 additions & 76 deletions src/apm/views/Redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,95 +6,26 @@ module ProcessOut {
}

export class APMViewRedirect extends APMViewImpl<RedirectProps> {
/** After a retryable headless error (e.g. pop-up blocked), show the normal Pay / Cancel UI. */
private headlessManualFallback = false

styles = css`
.redirect-headless-loading {
justify-content: center;
align-items: center;
flex-direction: column;
gap: 8px;
min-height: 120px;
}
.redirect-headless-empty {
min-height: 0;
margin: 0;
padding: 0;
overflow: hidden;
}
`

protected componentDidMount(): void {
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
if (headless && !this.headlessManualFallback) {
this.handleRedirectClick()
}
}

handleRedirectClick() {
ContextImpl.context.events.emit('redirect-initiated')
const pm = this.props.config.payment_method
const actionOptions = pm
? new ActionHandlerOptions(
pm.gateway_name.toLowerCase(),
pm.logo && pm.logo.light_url ? pm.logo.light_url.raster : undefined,
)
: new ActionHandlerOptions()
const redir = ContextImpl.context.redirect
if (redir && redir.actionOverlayMountParent != null) {
actionOptions.overlayMountParent = redir.actionOverlayMountParent
}
ContextImpl.context.poClient.handleAction(
this.props.config.redirect.url,
this.props.config.redirect.url,
() => {
ContextImpl.context.events.emit('redirect-completed')
ContextImpl.context.page.load(APIImpl.getCurrentStep)
},
(err) => {
const failure = {
message: err.message,
code: err.code,
}
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
if (headless) {
if (!this.headlessManualFallback && failure.code === 'customer.popup-blocked') {
this.headlessManualFallback = true
this.forceUpdate()
return
ContextImpl.context.events.emit('failure', {
failure: {
message: err.message,
code: err.code
}
if (this.headlessManualFallback) {
ContextImpl.context.events.emit('failure', { failure })
return
}
const silentFailureView = !!(redir && redir.silentFailureView)
ContextImpl.context.page.criticalFailure(
{
title: 'Could not open payment',
code: failure.code,
message: failure.message,
},
{ renderErrorView: !silentFailureView },
)
return
}
ContextImpl.context.events.emit('failure', { failure })
},
actionOptions,
})
}
)
}

render() {
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
if (headless && !this.headlessManualFallback) {
const showLoader =
!ContextImpl.context.redirect || ContextImpl.context.redirect.showHeadlessLoader !== false
if (showLoader) {
return page({ className: 'redirect-headless-loading' }, Loader())
}
return page({ className: 'redirect-headless-empty', 'aria-hidden': 'true' })
}

const redirectLabel = `Pay ${formatCurrency(this.props.config.invoice.amount, this.props.config.invoice.currency)}`;
return (
Main({
Expand Down
18 changes: 4 additions & 14 deletions src/processout/actionhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ module ProcessOut {
protected element: HTMLElement;
protected iframe: HTMLIFrameElement;
public closed: boolean;
protected mountParent?: HTMLElement;

constructor(el: HTMLElement, iframe: HTMLIFrameElement, mountParent?: HTMLElement) {
constructor(el: HTMLElement, iframe: HTMLIFrameElement) {
this.element = el;
this.iframe = iframe;
this.mountParent = mountParent;

window.addEventListener("resize", function(event) {
var width = Math.max(document.body.clientWidth,
Expand Down Expand Up @@ -44,7 +42,7 @@ module ProcessOut {
document.body.style.overflow = "hidden";

// And finally show the modal to the user
(this.mountParent || document.body).appendChild(this.element);
document.body.appendChild(this.element);
}

public close() {
Expand Down Expand Up @@ -82,9 +80,6 @@ module ProcessOut {
// gatewayLogo is shown when the action is done on another tab or window
public gatewayLogo?: string;

/** When set, iframe modal and new-window overlay are appended here instead of document.body */
public overlayMountParent?: HTMLElement;

public static ThreeDSChallengeFlow = "three-d-s-challenge-flow";
public static ThreeDSChallengeFlowNoIframe = "three-d-s-challenge-flow-no-iframe";
public static ThreeDSFingerprintFlow = "three-d-s-fingerprint-flow";
Expand Down Expand Up @@ -253,11 +248,7 @@ module ProcessOut {

iframeWrapper.appendChild(iframe);
iframeWrapper.appendChild(buttonWrapper);
this.iframeWrapper = new MockedIFrameWindow(
iframeWrapper,
iframe,
this.options.overlayMountParent,
);
this.iframeWrapper = new MockedIFrameWindow(iframeWrapper, iframe);
button.onclick = function() {
this.iframeWrapper.close();
}.bind(this);
Expand Down Expand Up @@ -473,8 +464,7 @@ module ProcessOut {
this.cancel();
}.bind(this), false);

var mount = this.options.overlayMountParent || document.body;
mount.appendChild(ret.topLayer);
document.body.appendChild(ret.topLayer);
return ret;
}

Expand Down
Loading