Hello, everyone ![]()
I have created a Shopify checkout UI extension. These are the dependencies:
- preact: 10.10.x,
- @preact/signals: 2.3.x,
- @shopify/ui-extensions: 2025.10.x
- target = “purchase.checkout.payment-method-list.render-after”
In my checkout UI extension, I am making it so that when a user wants an invoice, if they are a normal person, they must enter their DNI (in Spain, this is the national identity document), and if they are a company, they must enter their name and company code.
Currently, in my component, I have a pattern to check whether what the user enters is in the correct format or not, and if it is not in the correct format, a message is displayed in red and the input field turns red.
But I need help with something I don’t know how to do and can’t find a way to do in the documentation.
I want to make it so that when the user enters their ID incorrectly and the red text and red input are activated, the checkout purchase button is blocked and the user cannot complete the purchase until they fix the input. I want to do the same thing if they enter the company identifier (I don’t have the pattern for this company thing).
Could you please help me?
I am leaving the code for my component so that you can check if I am missing anything.
Thank you very much.
This is my component:
P.S.: If you cannot see the component clearly, I have uploaded it to Google Drive.
// @ts-nocheck
import ‘
/ui-extensions/preact’;
import { render, h } from “preact”;
import { useState, useEffect } from “preact/hooks”;
// Validador DNI/NIE español
function validarDNIoNIE(input) {
if (!input || typeof input !== ‘string’) return false;
const raw = input.replace(/[\s-]/g, ‘’).toUpperCase();
const LETTERS = ‘TRWAGMYFPDXBNJZSQVHLCKE’;
// DNI: 8 números + letra
if (/^\d{8}[A-Z]$/.test(raw)) {
const num = parseInt(raw.slice(0, 8), 10);
return LETTERS[num % 23] === raw[8];
}
// NIE: X/Y/Z + 7 números + letra
if (/
XYZ\d{7}[A-Z]$/.test(raw)) {
const pref = raw[0] === ‘X’ ? ‘0’ : raw[0] === ‘Y’ ? ‘1’ : ‘2’;
const num = parseInt(pref + raw.slice(1, 8), 10);
return LETTERS[num % 23] === raw[8];
}
return false;
}
export default async () => {
render(, document.body)
};
function Extension() {
const [quieroFactura, setQuieroFactura] = useState(false);
const [particular, setParticular] = useState(false);
const [empresa, setEmpresa] = useState(false);
const [dni, setDni] = useState(“”);
const [razonSocial, setRazonSocial] = useState(“”);
const [gif, setGif] = useState(“”);
const [dniError, setDniError] = useState(“”);
// si el total del pedido es superior a 400€ activo la factura
useEffect(() => {
let total_pedido = shopify.cost?.totalAmount?.v?.amount ?? 0.00;
if (total_pedido > 400) {
setQuieroFactura(true);
}
},
);
// actualizo las notas del pedido con los inputs
useEffect(() => {
if (dni && validarDNIoNIE(dni)) {
const objeto_dni = { input: “dni”, nif: dni };
shopify.applyNoteChange({ type: “updateNote”, note: JSON.stringify(objeto_dni) });
}
if (razonSocial || gif) {
const objeto_empresa = {
input: "empresa", // Un 'input' genérico para identificarlo
razon_social: razonSocial,
gif: gif
};
shopify.applyNoteChange({ type: "updateNote", note: JSON.stringify(objeto_empresa) });
}
}, [dni, razonSocial, gif]);
// Registrar validación personalizada
useEffect(() => {
// shopify.validation.register toma un callback que retorna un string (error) o null (válido)
shopify.validation.register(() => {
if (dni.length >= 9 && !validarDNIoNIE(dni)) {
return shopify.i18n.translate(“dniInvalido”);
}
return null;
});
// Limpia la validación al desmontar
return () => shopify.validation.unregister();
}, [dni]);
// funciones para bloquear los otros checkboxes
const handleParticularChange = (e) => {
setParticular(e.target.checked);
// si marco particular, desmarco empresa
if (e.target.checked) {
setEmpresa(false);
}
};
const handleEmpresaChange = (e) => {
setEmpresa(e.target.checked);
// si marco empresa, desmarco particular
if (e.target.checked) {
setParticular(false);
}
};
return (
<s-checkbox
label={shopify.i18n.translate(“factura”)}
id=“factura”
checked={quieroFactura}
onChange={e => setQuieroFactura(e.target.checked)}
/>
{quieroFactura && (
<s-stack direction="inline" gap="base">
{/* checkbox particular */}
<s-stack gap="base">
<s-checkbox
label={shopify.i18n.translate("particular")}
id="particular"
checked={particular}
onChange={handleParticularChange}
/>
</s-stack>
{/* checkbox empresa */}
<s-stack gap="base">
<s-checkbox
label={shopify.i18n.translate("empresa")}
id="empresa"
checked={empresa}
onChange={handleEmpresaChange}
/>
</s-stack>
{/* renderizado condicional del input dni */}
<s-stack>
{particular && (
<s-stack>
<s-text>{shopify.i18n.translate("dni")}</s-text>
<s-text-field
label={shopify.i18n.translate("nif")}
id="dni"
value={dni}
error={dniError || undefined}
onInput={e => {
const valor = e.target.value.toUpperCase();
setDni(valor);
if (valor.length >= 9) {
setDniError(validarDNIoNIE(valor) ? "" : shopify.i18n.translate("dniInvalido"));
} else {
setDniError("");
}
}}
/>
</s-stack>
)}
</s-stack>
{/* renderizado condicional de los inputs razon social y gif */}
<s-stack direction="inline" gap="base">
{empresa && (
<>
<s-text-field
label={shopify.i18n.translate("razonSocial")}
id="razon-social"
value={razonSocial}
onInput={e => setRazonSocial(e.target.value)}
/>
<s-text-field
label={shopify.i18n.translate("gif")}
id="gif"
value={gif}
onInput={e => setGif(e.target.value)}
/>
</>
)}
</s-stack>
</s-stack>
)}
</s-stack>
);
}