You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
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.
src/Services/UnifiedApiHostedPaymentService.php:91-102 — base64_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-60 — extractRedirectUrl() 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.
This file contains hidden or 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
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
redirectHtmlalongside the existingredirectUrlonHostedPaymentOutput, covering the Unified API's "recommended for web" 3DS-pending response shape.Previously,
UnifiedApiHostedPaymentService::createHostedPayment()only extractedredirect.urlfrom the Unified API response — a shape that's actually only reachable when the request setscard.threeDSecure.displayMode=raw. By default (nodisplayModeoverride), the API instead returns a Base64-encodedredirect.htmlblock containing a self-submitting form that sends the end user to the bank's challenge page. This PR adds that extraction path:HostedPaymentOutputgains a new?string $redirectHtmlproperty (4th constructor param, defaults tonullfor backward compatibility).UnifiedApiHostedPaymentServicegains a privateextractRedirectHtml(), mirroringextractRedirectUrl(): readsredirect.htmlfrom the JSON body, Base64-decodes it, and returnsnull(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.mdupdated in the same PR to document the new field/behavior per the repo's own documentation-sync rule.Neither
redirectUrlnorredirectHtmlmaps to aPaymentOutcome— that mapping is deferred to the asynchronous webhook/3DS-return confirmation flow (PRE-3588).Related Issue
Ticket: PRE-3551
Type of Change
✅ Quality Checklist
Local Environment & Hooks
make install).(PRE|SMP)-XXXX: descriptionpattern.(feature|fix|hotfix|refactor)/(PRE|SMP)-XXXX...or(release|patch)/x.y.z.Testing & Code Quality
make cs-fix).make stan— PHPStan level 8).make test).src/ortests/(no typed properties, arrowfunctions, constructor property promotion,
match,enum).CI/CD Deployment Context
compatibilitymatrix(PHP 7.1 / 7.4 / 8.0 / 8.1 / 8.2) and the
qualityjob.Notes for Reviewer
redirectHtml's default (= null) on the constructor keeps this non-breaking for any existing caller constructingHostedPaymentOutputdirectly.redirect.html, and non-stringredirect.html— mirroring the existingredirectUrltest cases.redirect.postParams(needed to actually useredirectUrlin raw-mode) is still not extracted — out of scope here, unchanged from prior behavior.