Skip to content

PRE-3551: add returns urls on payment - #26

Merged
adumont-payplug merged 1 commit into
release/1.0.0from
feature/PRE-3551_hosted_payment_redirect_html
Aug 17, 2026
Merged

PRE-3551: add returns urls on payment#26
adumont-payplug merged 1 commit into
release/1.0.0from
feature/PRE-3551_hosted_payment_redirect_html

Conversation

@adumont-payplug

@adumont-payplug adumont-payplug commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds redirectHtml alongside the existing redirectUrl on HostedPaymentOutput, covering the Unified API's "recommended for web" 3DS-pending response shape.

Previously, UnifiedApiHostedPaymentService::createHostedPayment() only extracted redirect.url from the Unified API response — a shape that's actually only reachable when the request sets card.threeDSecure.displayMode=raw. By default (no displayMode override), the API instead returns a Base64-encoded redirect.html block containing a self-submitting form that sends the end user to the bank's challenge page. This PR adds that extraction path:

  • HostedPaymentOutput gains a new ?string $redirectHtml property (4th constructor param, defaults to null for backward compatibility).
  • UnifiedApiHostedPaymentService gains a private extractRedirectHtml(), mirroring extractRedirectUrl(): reads redirect.html from the JSON body, Base64-decodes it, and returns null (rather than throwing) on malformed JSON, a missing/non-string field, or invalid Base64 — same "extract one derived field, don't validate the whole payload" reasoning as the existing method.
  • CLAUDE.md updated in the same PR to document the new field/behavior per the repo's own documentation-sync rule.

Neither redirectUrl nor redirectHtml maps to a PaymentOutcome — that mapping is deferred to the asynchronous webhook/3DS-return confirmation flow (PRE-3588).

Related Issue

Ticket: PRE-3551

Type of Change

  • ✨ New feature

✅ Quality Checklist

Local Environment & Hooks

  • Local Git hooks (CaptainHook) are installed and executed cleanly (make install).
  • Commit messages strictly follow the (PRE|SMP)-XXXX: description pattern.
  • Branch name follows (feature|fix|hotfix|refactor)/(PRE|SMP)-XXXX... or (release|patch)/x.y.z.

Testing & Code Quality

  • Coding style rules have been applied locally (make cs-fix).
  • Static analysis passes with no new regressions (make stan — PHPStan level 8).
  • I have added/updated PHPUnit tests if applicable (make test).
  • No PHP syntax newer than 7.1 introduced in src/ or tests/ (no typed properties, arrow
    functions, constructor property promotion, match, enum).

CI/CD Deployment Context

  • The CI pipeline passes fully on GitHub, including the compatibility matrix
    (PHP 7.1 / 7.4 / 8.0 / 8.1 / 8.2) and the quality job.

Notes for Reviewer

  • redirectHtml's default (= null) on the constructor keeps this non-breaking for any existing caller constructing HostedPaymentOutput directly.
  • Test coverage added for: successful decode, non-Base64 redirect.html, and non-string redirect.html — mirroring the existing redirectUrl test cases.
  • redirect.postParams (needed to actually use redirectUrl in raw-mode) is still not extracted — out of scope here, unchanged from prior behavior.

@adumont-payplug

Copy link
Copy Markdown
Collaborator Author

Code Review

Overview

Adds a redirectHtml field to HostedPaymentOutput, extracted by a new extractRedirectHtml() private method on UnifiedApiHostedPaymentService, mirroring the existing extractRedirectUrl(). This covers the Unified API's default (non-raw-mode) 3DS-pending response shape: a Base64-encoded redirect.html block containing a self-submitting challenge form. Change is additive and backward-compatible ($redirectHtml = null default on the constructor). CLAUDE.md is updated to match, per the project's own doc-sync convention. 4 files changed, +114/-17.


Security — ✅ No issues

  • Data source is Payplug's own Unified API over TLS (not user input), so decoding and returning raw HTML for the CMS plugin to inject is the documented, intended flow (the 3DS doc explicitly recommends this shape). No injection surface introduced by this library itself — sanitization/escaping responsibility correctly stays with the consuming CMS plugin, consistent with this library's existing "extract, don't validate the full payload" stance.
  • base64_decode($str, true) correctly uses strict mode, avoiding silent garbage-decoding of malformed input.

Correctness — ⚠️ One edge case

  • src/Services/UnifiedApiHostedPaymentService.php:91-102base64_decode('', true) returns '' (empty string), not false. If the API ever returns "redirect": {"html": ""}, extractRedirectHtml() returns "" instead of null, unlike extractRedirectUrl()'s equivalent case (an empty string there would also pass through as "", so this is consistent with the existing sibling method, but neither method treats an empty result as "absent"). Low likelihood in practice (the API isn't documented to send an empty html field), but worth a one-line guard ('' !== $decoded) if you want "null means no redirect" to hold strictly. Not blocking.
  • Good: malformed JSON, missing field, non-string value, and invalid Base64 are all correctly handled and tested.

Performance — ℹ️ Minor

  • src/Services/UnifiedApiHostedPaymentService.php:59-60extractRedirectUrl() and extractRedirectHtml() each independently call json_decode($body, true) on the same $response['body'], so the response is parsed twice per createHostedPayment() call. Negligible for a single payment-creation response, but if a third extract* method is added later for a future field, consider decoding once and passing the array to all three extractors rather than tripling the parse.

Maintainability — ℹ️ Minor

  • src/Output/HostedPaymentOutput.php:14 — the docblock reference URL is literally https://payplug.gitbook.io/payplug/.../3d-secure-implementation/using-payplugs-3ds-module — the ... looks like a truncated copy-paste rather than a real path segment, so the link as written isn't followable. Worth fixing to the real gitbook path (CLAUDE.md's own reference to the same doc uses the path without a literal ellipsis, so the full URL is presumably known).
  • extractRedirectHtml() closely mirrors extractRedirectUrl() (same guard-clause shape) — acceptable duplication at 2 methods, consistent with the project's stated preference for simple duplication over premature abstraction; not flagging as a blocker.

Test Coverage — ✅ Good

  • New tests cover: successful decode, invalid-Base64 input, non-string value, and the existing "not valid JSON" / direct-success tests were extended to also assert redirectHtml is null. This mirrors the existing redirectUrl test suite well.
  • Gap: no test for redirect.html present as an empty string "" (see Correctness note above) — add one if you tighten that behavior.

Documentation

  • CLAUDE.md updates accurately reflect the new field, its two source shapes (redirect.html vs redirect.url+postParams), and correctly continue to defer PaymentOutcome mapping to PRE-3588. Matches the repo's Architecture-section convention.

Summary

Solid, well-tested, low-risk additive change that closes a real gap (the previous code only handled the raw-mode shape, which isn't what the API returns by default). Nothing blocking; two nit-level suggestions (empty-string edge case, broken docblock URL) worth a quick follow-up but not worth holding the PR for.

@adumont-payplug
adumont-payplug force-pushed the feature/PRE-3551_hosted_payment_redirect_html branch from ba1dfa1 to ffeb2ae Compare August 17, 2026 08:25
@adumont-payplug
adumont-payplug merged commit 112c249 into release/1.0.0 Aug 17, 2026
14 checks passed
@adumont-payplug
adumont-payplug deleted the feature/PRE-3551_hosted_payment_redirect_html branch August 17, 2026 08:26
@adumont-payplug adumont-payplug mentioned this pull request Aug 17, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants