Summary
When a native alternative payment (v2 flow) performs a web-type redirect and the redirect fails — the customer closes ASWebAuthenticationSession, or the session fails to start (e.g. HTTPS callback used without the required webcredentials associated domain) — the SDK surfaces processout-mobile.cancelled to the merchant app, but never informs the backend. The invoice remains in its pre-redirect pending state in the dashboard, so the SDK-reported outcome and the invoice state disagree, which breaks merchant-side reconciliation.
Details
In NativeAlternativePaymentDefaultInteractor.uncheckedRedirect(to:):
deep_link branch: a failed redirect IS reported — redirectResult = redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, sent via continuePayment.
web branch: webAuthenticationSession.authenticate(using:) throws, execution never reaches continuePayment, and the failure is only set locally via setFailureState. The backend gets nothing, even when the response's redirect.confirmationRequired is true.
So the two redirect types are inconsistent: the protocol has a redirect confirmation payload (PONativeAlternativePaymentRedirectResultV2(success:)) and the backend opts into it via confirmationRequired, but web redirect failures bypass it entirely.
Note that the failure is visible in SDK telemetry (setFailureState logs at warn with the invoice ID attached, which reaches the telemetry endpoint) — but telemetry doesn't transition invoice state, so the dashboard still shows the payment as pending until expiry.
The Android SDK has the same asymmetry (handleWebRedirect(failure:) completes locally without sending PONativeAlternativePaymentRedirectConfirmation(success = false)), so this appears to be a protocol-level gap rather than a platform quirk.
Steps to reproduce
- Start a native APM payment whose gateway responds with a redirect of
type: "web" and confirmationRequired: true.
- Close the presented
ASWebAuthenticationSession (or make it fail to start).
- Observe: app receives
POFailure with code processout-mobile.cancelled; the invoice in the dashboard remains pending — no state change, no record of the failed redirect attempt.
Expected behavior
When redirect.confirmationRequired is true and the web redirect fails, the SDK should report redirect: .init(success: false) via continuePayment (mirroring the deep link branch) before surfacing the local failure, so the backend can transition the invoice accordingly.
Environment
- Observed on 4.42.0; the relevant code is unchanged on current
master (post-4.44.0).
- Context: we (Glovo) hit this at scale when web redirect sessions failed to start, and payments our app reported as failed/cancelled stayed pending in the dashboard.
Proposed fix
PR incoming that mirrors the deep link semantics (gated on confirmationRequired, best-effort report, original error still propagated unchanged). Two points we'd like your guidance on in review:
- What the backend does on
success: false for web redirects (fail the attempt vs. allow retry) — the deep link precedent suggests it's handled, but please confirm.
- Whether the report should also be attempted from a cancelled task context (user-cancel path), which would require an unstructured task to survive cancellation.
Additionally, the confirmation payload only carries success: Bool — an optional failure reason field could be a useful protocol extension so the backend can distinguish customer cancellation from technical failures, but that's beyond this PR's scope.
Summary
When a native alternative payment (v2 flow) performs a
web-type redirect and the redirect fails — the customer closesASWebAuthenticationSession, or the session fails to start (e.g. HTTPS callback used without the requiredwebcredentialsassociated domain) — the SDK surfacesprocessout-mobile.cancelledto the merchant app, but never informs the backend. The invoice remains in its pre-redirect pending state in the dashboard, so the SDK-reported outcome and the invoice state disagree, which breaks merchant-side reconciliation.Details
In
NativeAlternativePaymentDefaultInteractor.uncheckedRedirect(to:):deep_linkbranch: a failed redirect IS reported —redirectResult = redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, sent viacontinuePayment.webbranch:webAuthenticationSession.authenticate(using:)throws, execution never reachescontinuePayment, and the failure is only set locally viasetFailureState. The backend gets nothing, even when the response'sredirect.confirmationRequiredistrue.So the two redirect types are inconsistent: the protocol has a redirect confirmation payload (
PONativeAlternativePaymentRedirectResultV2(success:)) and the backend opts into it viaconfirmationRequired, but web redirect failures bypass it entirely.Note that the failure is visible in SDK telemetry (
setFailureStatelogs atwarnwith the invoice ID attached, which reaches the telemetry endpoint) — but telemetry doesn't transition invoice state, so the dashboard still shows the payment as pending until expiry.The Android SDK has the same asymmetry (
handleWebRedirect(failure:)completes locally without sendingPONativeAlternativePaymentRedirectConfirmation(success = false)), so this appears to be a protocol-level gap rather than a platform quirk.Steps to reproduce
type: "web"andconfirmationRequired: true.ASWebAuthenticationSession(or make it fail to start).POFailurewith codeprocessout-mobile.cancelled; the invoice in the dashboard remains pending — no state change, no record of the failed redirect attempt.Expected behavior
When
redirect.confirmationRequiredistrueand the web redirect fails, the SDK should reportredirect: .init(success: false)viacontinuePayment(mirroring the deep link branch) before surfacing the local failure, so the backend can transition the invoice accordingly.Environment
master(post-4.44.0).Proposed fix
PR incoming that mirrors the deep link semantics (gated on
confirmationRequired, best-effort report, original error still propagated unchanged). Two points we'd like your guidance on in review:success: falsefor web redirects (fail the attempt vs. allow retry) — the deep link precedent suggests it's handled, but please confirm.Additionally, the confirmation payload only carries
success: Bool— an optional failure reason field could be a useful protocol extension so the backend can distinguish customer cancellation from technical failures, but that's beyond this PR's scope.