Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0659dc6
PRE-3550: add hostedFields gateway config flag and credential fields
jhoaraupp Aug 3, 2026
0b6904a
PRE-3550: reject combining integratedPayment and hostedFields on the …
jhoaraupp Aug 3, 2026
f06d36d
PRE-3550: register admin form hook for Hosted Fields configuration
jhoaraupp Aug 3, 2026
d2ba4f0
PRE-3550: add HostedFieldsPaymentProcessorInterface with a no-op stub…
jhoaraupp Aug 3, 2026
8cd627d
PRE-3550: relay Hosted Fields token to HostedFieldsPaymentProcessorIn…
jhoaraupp Aug 3, 2026
a354361
PRE-3550: add Hosted Fields shop checkout template
jhoaraupp Aug 3, 2026
98d4eda
PRE-3550: add Hosted Fields Stimulus controller
jhoaraupp Aug 3, 2026
f3b04e4
PRE-3550: add Behat coverage for Hosted Fields checkout visibility
jhoaraupp Aug 3, 2026
1a2d791
PRE-3550: allow several payment methods on the payplug gateway factory
jhoaraupp Aug 3, 2026
6008de4
PRE-3550: only accept a Hosted Fields token when the flag is enabled
jhoaraupp Aug 3, 2026
0d6d1b4
PRE-3550: mount the Hosted Fields iframes only once the method is sel…
jhoaraupp Aug 3, 2026
009bdc9
PRE-3550: mock the PayPlug account lookup in the Hosted Fields Behat …
jhoaraupp Aug 3, 2026
55af9d6
PRE-3550: keep Hosted Fields away from Payum after checkout completion
jhoaraupp Aug 3, 2026
d759ca5
PRE-3550: align the redirect precedence with handle()'s dispatch order
jhoaraupp Aug 3, 2026
4701729
PRE-3550: add oneClick to the payplug_uhf gateway configuration
jhoaraupp Aug 7, 2026
fba7c85
PRE-3550: add PaymentMethodValidator::processUhf() with a oneClick pe…
jhoaraupp Aug 7, 2026
8f4365b
PRE-3550: repoint the Hosted Fields shop flow onto payplug_uhf
jhoaraupp Aug 7, 2026
086043b
PRE-3550: remove the flag-based Hosted Fields implementation on payplug
jhoaraupp Aug 7, 2026
0baf90a
PRE-3550: register the payplug_uhf twig hook so Hosted Fields checkou…
jhoaraupp Aug 7, 2026
bd9655b
PRE-3550: JS-escape values interpolated into the Hosted Fields inline…
jhoaraupp Aug 7, 2026
712f32f
PRE-3550: remove the now-unjustified payplug exemption in canBeCreated()
jhoaraupp Aug 7, 2026
d7bd4bb
PRE-3550: assert Hosted Fields markup actually renders in the Behat s…
jhoaraupp Aug 7, 2026
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
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ install-sylius:
${COMPOSER} require --dev sylius/test-application:"^${SYLIUS_VERSION}@alpha" -n -W # TODO: Remove alpha when stable
${COMPOSER} test-application:install



behat-configure: ## Configure Behat
(cd ${TEST_DIRECTORY} && cp behat.yml.dist behat.yml)
(cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/src/Sylius/Behat/Resources/config/suites.yml#vendor/${PLUGIN_NAME}/tests/Behat/Resources/suites.yml#g" behat.yml)
Expand Down
7 changes: 7 additions & 0 deletions assets/controllers.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
"@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true
}
},
"hosted-fields": {
"enabled": true,
"fetch": "lazy",
"autoimport": {
"@payplug/sylius-payplug-plugin/shop/dist/payment/hosted-fields.css": true
}
},
"oney-payment": {
"enabled": true,
"fetch": "lazy"
Expand Down
9 changes: 9 additions & 0 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
"@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true
}
},
"hosted-fields": {
"main": "shop/controllers/hosted-fields_controller.js",
"webpackMode": "lazy",
"fetch": "lazy",
"enabled": true,
"autoimport": {
"@payplug/sylius-payplug-plugin/shop/dist/payment/hosted-fields.css": true
}
},
"oney-payment": {
"main": "shop/controllers/oney-payment_controller.js",
"webpackMode": "lazy",
Expand Down
170 changes: 170 additions & 0 deletions assets/shop/controllers/hosted-fields_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { Controller } from '@hotwired/stimulus';

const ALLOWED_BRANDS = ['CB', 'VISA', 'MASTERCARD'];

// Applied inside each hosted iframe (Dalenys renders these rules into the field's own
// document). The SDK only accepts a small whitelist of CSS properties here — anything
// outside it (we tried "height": SDK logged "Css property ... is not supported" and threw,
// which aborted ALL fields, not just the one it complained about) — so stick to exactly the
// properties confirmed by PayPlug's own documented example (font-size/color/font-style).
// Background and sizing/centering of the field's content are NOT controllable this way.
const FIELD_STYLE = {
input: {
'font-size': '14px',
color: '#2B343D',
'background-color': 'transparent',
},
'::placeholder': {
'font-size': '14px',
color: '#969a9f',
},
};

/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['container', 'error', 'submitButton'];

connect() {
if (typeof payplug_hosted_fields_params === 'undefined') {
return;
}

this.form = this.element.closest('form');
this.hfields = null;

if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.addEventListener('click', (event) => {
event.preventDefault();
this.tokenizeAndSubmit();
});
}

// Stimulus connects as soon as the markup is in the DOM, even though the payment method
// container starts hidden (see shop/select_payment/choice.html.twig). Mounting the
// cross-origin Dalenys iframes into a display:none container breaks their rendering, so
// load them only once this payment method is actually selected.
const isChecked = this.getPaymentMethodSelectors({
methodCode: payplug_hosted_fields_params.payment_method_code,
checked: true,
});
if (isChecked.length) {
this.openFields();
}

this.getPaymentMethodSelectors().forEach((element) => {
element.addEventListener('change', (e) => {
if (payplug_hosted_fields_params.payment_method_code === e.currentTarget.value && e.currentTarget.checked) {
this.openFields();
}
});
});
}

handleShow(event) {
if (this.hasContainerTarget) {
import('jquery').then(({ default: $ }) => {
$(this.containerTarget).slideDown();
});
this.openFields();
this.containerTarget.dataset.paymentInlineSubmit = "true";
this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true }));
}
}

handleHide(event) {
if (this.hasContainerTarget) {
import('jquery').then(({ default: $ }) => {
$(this.containerTarget).slideUp();
});
this.closeFields();
this.containerTarget.dataset.paymentInlineSubmit = "false";
this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true }));
}
}

getPaymentMethodSelectors({ methodCode, checked } = {}) {
const baseSelector = '[id*=checkout_select_payment_payments]';

if (methodCode) {
if (checked) {
return document.querySelectorAll(`${baseSelector}[value=${methodCode}]:checked`);
}
return document.querySelectorAll(`${baseSelector}[value=${methodCode}]`);
}
return document.querySelectorAll(baseSelector);
}

openFields() {
if (this.hasContainerTarget) {
this.containerTarget.classList.add('payplugHostedFields--loaded');
}
if (null === this.hfields) {
this.load();
}
}

closeFields() {
if (this.hasContainerTarget) {
this.containerTarget.classList.remove('payplugHostedFields--loaded');
}
}

load() {
this.hfields = window.dalenys.hostedFields({
companyId: payplug_hosted_fields_params.companyId,
fields: {
brand: { id: 'brand-container', style: FIELD_STYLE },
card: { id: 'card-container', style: FIELD_STYLE },
expiry: { id: 'expiry-container', style: FIELD_STYLE },
cryptogram: { id: 'cvv-container', style: FIELD_STYLE },
},
location: payplug_hosted_fields_params.locale,
});
this.hfields.load();
}

tokenizeAndSubmit() {
if (null === this.hfields) {
// Fields were never mounted (payment method not selected yet): nothing to tokenize.
return;
}

this.hideError();
this.hfields.createToken((result) => {
if (result.execCode !== '0000') {
this.showError(payplug_hosted_fields_params.error.tokenization_failed);
return;
}

const selectedBrand = (result.selectedBrand || '').toUpperCase();
if (!ALLOWED_BRANDS.includes(selectedBrand)) {
this.showError(payplug_hosted_fields_params.error.unsupported_brand);
return;
}

const saveCardElement = this.element.querySelector('#hostedfields_savecard');
const saveCard = null !== saveCardElement && saveCardElement.checked;

this.form.querySelector('#hostedfields_token').value = result.hfToken;
this.form.querySelector('#hostedfields_selected_brand').value = selectedBrand;
this.form.querySelector('#hostedfields_save_card').value = saveCard ? 'true' : 'false';
this.form.submit();
});
}

showError(message) {
if (!this.hasErrorTarget) {
return;
}
this.errorTarget.textContent = message;
this.errorTarget.classList.remove('payplugHostedFields__error--hide');
}

hideError() {
if (!this.hasErrorTarget) {
return;
}
this.errorTarget.textContent = '';
this.errorTarget.classList.add('payplugHostedFields__error--hide');
}
}
169 changes: 169 additions & 0 deletions assets/shop/dist/payment/hosted-fields.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
.payplugHostedFields {
justify-self: center;
display: none
}

.payplugHostedFields * {
font-family: Poppins, Arial, sans-serif !important
}

.payplugHostedFields--loaded {
width: 100%;
max-width: 400px;
flex-wrap: wrap;
justify-content: space-between;
margin: 20px auto 0;
display: flex;
position: relative
}

.payplugHostedFields__container {
width: 100%;
margin: 0 0 10px;
padding: 0;
display: flex;
position: relative
}

.payplugHostedFields__container--brand,
.payplugHostedFields__container--card,
.payplugHostedFields__container--expiry,
.payplugHostedFields__container--cvv {
height: 40px;
cursor: text;
border: 1px solid #d5d6d8;
border-radius: 2px;
line-height: 40px
}

.payplugHostedFields__container--card,
.payplugHostedFields__container--expiry,
.payplugHostedFields__container--cvv {
padding: 0 16px 0 50px
}

.payplugHostedFields__container--card:before,
.payplugHostedFields__container--expiry:before,
.payplugHostedFields__container--cvv:before {
content: "";
width: 24px;
height: 24px;
background: #95999e 50%/100% no-repeat;
position: absolute;
top: 20%;
left: 16px
}

.payplugHostedFields__container--card:before {
-webkit-mask-image: url(card.0d2bd9bc.svg);
mask-image: url(card.0d2bd9bc.svg)
}

.payplugHostedFields__container--expiry:before {
-webkit-mask-image: url(calendar.3c23bb16.svg);
mask-image: url(calendar.3c23bb16.svg)
}

.payplugHostedFields__container--cvv:before {
-webkit-mask-image: url(lock.fe8a73cd.svg);
mask-image: url(lock.fe8a73cd.svg)
}

.payplugHostedFields__container--expiry,
.payplugHostedFields__container--cvv {
max-width: calc(50% - 2px);
display: inline-block
}

.payplugHostedFields__container--brand,
.payplugHostedFields__container--card,
.payplugHostedFields__container--expiry,
.payplugHostedFields__container--cvv {
overflow: hidden
}

.payplugHostedFields__container--brand iframe,
.payplugHostedFields__container--card iframe,
.payplugHostedFields__container--expiry iframe,
.payplugHostedFields__container--cvv iframe {
width: 100%;
height: 100%;
border: none;
display: block
}

.payplugHostedFields__container--saveCard {
height: auto;
align-items: center;
padding: 10px 0 0;
display: flex
}

.payplugHostedFields__container--saveCard input {
display: none
}

.payplugHostedFields__container--saveCard input:checked+label span:before {
opacity: 1
}

.payplugHostedFields__container--saveCard label {
cursor: pointer;
color: #918f8f;
margin: 0 !important;
font-size: 12px !important
}

.payplugHostedFields__container--saveCard label span {
cursor: pointer;
height: 16px;
-o-transition: border .4s;
width: 16px;
border: 1px solid #d5d6d8;
border-radius: 2px;
margin: 0 10px -3px 0;
transition: border .4s;
display: inline-block;
position: relative
}

.payplugHostedFields__container--saveCard label span:before {
content: "";
height: 5px;
opacity: 0;
width: 10px;
border-top: none;
border-bottom: 2.5px solid #2b343d;
border-left: 2.5px solid #2b343d;
border-right: none;
border-radius: 1px;
transition: opacity .4s;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -55%)rotate(-48deg)
}

.payplugHostedFields__container--saveCard label:hover {
color: #2b343d;
transition: all .1s
}

.payplugHostedFields__container--saveCard label:hover span {
border-color: #2b343d;
transition: all .1s
}

.payplugHostedFields__error {
color: #e91932;
width: 100%;
margin: -10px 0 10px;
padding-left: 4px;
font-size: 12px;
line-height: 18px
}

.payplugHostedFields__error--hide {
display: none
}
5 changes: 5 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ services:
PayPlug\SyliusPayPlugPlugin\Provider\OneySimulation\OneySimulationDataProviderInterface:
class: PayPlug\SyliusPayPlugPlugin\Provider\OneySimulation\OneySimulationDataProvider

# Alias (not a separate definition) so the interface resolves to the service auto-registered by the
# `PayPlug\SyliusPayPlugPlugin\:` prototype above, keeping its `@monolog.logger.payplug` binding.
PayPlug\SyliusPayPlugPlugin\PaymentProcessing\HostedFieldsPaymentProcessorInterface:
alias: PayPlug\SyliusPayPlugPlugin\PaymentProcessing\NullHostedFieldsPaymentProcessor

payplug_sylius_payplug_plugin.action.capture:
class: PayPlug\SyliusPayPlugPlugin\Action\CaptureAction

Expand Down
Loading
Loading