Skip to content

Commit e2bbebb

Browse files
Add headless APM Payment redirect mode
1 parent 2eb8b9d commit e2bbebb

4 files changed

Lines changed: 77 additions & 9 deletions

File tree

docs/apm-ui-system.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,11 @@ const apm = client.apm.authorization(container, {
574574
timeout: 900, // Timeout in seconds (15 minutes)
575575
allowCancelation: true // Allow cancellation during confirmation
576576
},
577+
578+
// Redirect step (web): skip intermediate Pay / “Continue to payment” UI (same idea as mobile enableHeadlessMode)
579+
redirect: {
580+
enableHeadlessMode: false,
581+
},
577582

578583
// Success screen settings
579584
success: {

src/apm/Context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ module ProcessOut {
3939
/** Whether user must take action to dismiss success screen (default: false) */
4040
requiresAction: boolean
4141
}
42+
43+
/**
44+
* Client-side redirect behaviour (web APM). Aligns with mobile
45+
* `RedirectConfiguration.enableHeadlessMode`: skip the intermediate
46+
* “Continue to payment” / Pay button and open the PSP flow as soon as the
47+
* redirect step is shown.
48+
*/
49+
redirect?: {
50+
enableHeadlessMode?: boolean
51+
}
4252
}
4353

4454
export type TokenizationUserData = TokenizationFlowData & FlowData

src/apm/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ module ProcessOut {
3333
requiresAction: false,
3434
autoDismissDuration: 3,
3535
manualDismissDuration: 60,
36-
...data.success,
36+
...(data.success || {}),
3737
},
3838
confirmation: {
3939
requiresAction: false,
4040
timeout: MIN_15 / 1000,
4141
allowCancelation: true,
42-
...data.confirmation,
42+
...(data.confirmation || {}),
43+
},
44+
redirect: {
45+
enableHeadlessMode: false,
46+
...(data.redirect || {}),
4347
},
4448
logger: {
4549
error: (options: Omit<Parameters<TelemetryClient['reportError']>[0], 'stack'>) => {

src/apm/views/Redirect.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,75 @@ module ProcessOut {
66
}
77

88
export class APMViewRedirect extends APMViewImpl<RedirectProps> {
9+
/** After a retryable headless error (e.g. pop-up blocked), show the normal Pay / Cancel UI. */
10+
private headlessManualFallback = false
11+
12+
styles = css`
13+
.redirect-headless-loading {
14+
justify-content: center;
15+
align-items: center;
16+
flex-direction: column;
17+
gap: 8px;
18+
min-height: 120px;
19+
}
20+
`
21+
22+
protected componentDidMount(): void {
23+
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
24+
if (headless && !this.headlessManualFallback) {
25+
this.handleRedirectClick()
26+
}
27+
}
28+
929
handleRedirectClick() {
1030
ContextImpl.context.events.emit('redirect-initiated')
31+
const pm = this.props.config.payment_method
32+
const actionOptions = pm
33+
? new ActionHandlerOptions(
34+
pm.gateway_name.toLowerCase(),
35+
pm.logo && pm.logo.light_url ? pm.logo.light_url.raster : undefined,
36+
)
37+
: undefined
1138
ContextImpl.context.poClient.handleAction(
12-
this.props.config.redirect.url,
39+
this.props.config.redirect.url,
1340
() => {
1441
ContextImpl.context.events.emit('redirect-completed')
1542
ContextImpl.context.page.load(APIImpl.getCurrentStep)
1643
},
1744
(err) => {
18-
ContextImpl.context.events.emit('failure', {
19-
failure: {
20-
message: err.message,
21-
code: err.code
45+
const failure = {
46+
message: err.message,
47+
code: err.code,
48+
}
49+
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
50+
if (headless) {
51+
if (
52+
!this.headlessManualFallback &&
53+
(failure.code === 'customer.popup-blocked' || failure.code === 'customer.canceled')
54+
) {
55+
this.headlessManualFallback = true
56+
this.forceUpdate()
57+
return
2258
}
23-
})
24-
}
59+
ContextImpl.context.page.criticalFailure({
60+
title: 'Could not open payment',
61+
code: failure.code,
62+
message: failure.message,
63+
})
64+
return
65+
}
66+
ContextImpl.context.events.emit('failure', { failure })
67+
},
68+
actionOptions,
2569
)
2670
}
2771

2872
render() {
73+
const headless = ContextImpl.context.redirect && ContextImpl.context.redirect.enableHeadlessMode
74+
if (headless && !this.headlessManualFallback) {
75+
return page({ className: 'redirect-headless-loading' }, Loader())
76+
}
77+
2978
const redirectLabel = `Pay ${formatCurrency(this.props.config.invoice.amount, this.props.config.invoice.currency)}`;
3079
return (
3180
Main({

0 commit comments

Comments
 (0)