# Netpay3ds

Netpay3ds es una librería que facilita la integración de 3D Secure v2 de NetPay en
aplicaciones Angular compatibles. La librería inicializa Songbird, configura
Cardinal y coordina la autenticación frictionless o challenge; la tokenización,
el charge y el confirm continúan realizándose contra la integración de pagos de
la aplicación consumidora.

## Contents

- [Angular Compatibility](#angular-compatibility)
- [Which version should I use?](#which-version-should-i-use)
- [Installation](#installation--instalación)
- [Upgrading from 1.x to 2.x](#upgrading-from-1x-to-2x)
- [Quick Start](#quick-start)
- [TypeScript](#typescript)
- [JavaScript](#javascript)
- [Environment Configuration](#environment-configuration)
- [Dynamic 3DS Configuration](#dynamic-3ds-configuration)
- [3DS Authentication Flow](#3ds-authentication-flow)
- [Timeouts](#timeouts)
- [Error Handling](#error-handling)
- [API Reference](#api-reference)
- [Complete Example](#complete-example)
- [Migration](#migration-from-05x-to-1x)
- [Versioning and Legacy Support](#versioning)
- [License](#license)

## Angular Compatibility

Las dos líneas se publican bajo el mismo paquete npm, `netpay3ds`. Esta tabla
describe la compatibilidad de la aplicación consumidora; la versión de Angular
usada internamente para construir la librería no amplía estos rangos.

| Línea `netpay3ds` | Release publicado actual | Canal npm actual | Angular del consumer | RxJS del consumer | Dependencia runtime | Recomendado para |
| --- | --- | --- | --- | --- | --- | --- |
| 1.x | `1.1.1` | `legacy` | `>=13.2.0 <22.0.0` (Angular 13.2–21) | `>=7.5.0 <8.0.0` | `tslib ^2.3.0` | Aplicaciones existentes en Angular 13–21 |
| 2.x | `2.0.0` | `latest` | `>=22.0.0 <23.0.0` (Angular 22) | `>=7.5.0 <8.0.0` | `tslib ^2.3.0` | Nuevas integraciones y aplicaciones productivas en Angular 22 |

Rangos públicos declarados por 1.x:

```text
@angular/core   >=13.2.0 <22.0.0
@angular/common >=13.2.0 <22.0.0
rxjs            >=7.5.0 <8.0.0
```

Rangos públicos declarados por 2.x:

```text
@angular/core   >=22.0.0 <23.0.0
@angular/common >=22.0.0 <23.0.0
rxjs            >=7.5.0 <8.0.0
```

`tslib ^2.3.0` es una dependencia runtime de ambas líneas. `zone.js` no es una
peer dependency pública de `netpay3ds`; su versión corresponde a la aplicación
Angular consumidora. No instales la versión de `zone.js` usada por el workspace
de build como si fuera un requisito adicional del paquete.

La instalación, el linker Partial-Ivy, la API y el build productivo de 2.x se
validaron en un consumer limpio Angular 22. También se validó funcionalmente el
flujo hosted-checkout con un **3DS Challenge aprobado**. Angular 23 está
intencionalmente fuera del rango hasta completar una validación explícita.

## Which version should I use?

| Tu aplicación | Selección | Comando |
| --- | --- | --- |
| Angular 13.2–21 | Línea estable de compatibilidad 1.x | `npm install netpay3ds@legacy` |
| Angular 22 | Línea estable 2.x | `npm install netpay3ds@latest` |
| Angular 23 o superior | Aún no soportado por los peers actuales | No fuerces la instalación |
| Release exacto y reproducible | Fija una versión publicada | `npm install netpay3ds@<exact-version>` |

No uses `next` para producción. Es el canal de testing y validación prerelease
de futuros releases 2.x. Antes de instalar, puedes consultar el estado real de
los canales con:

```bash
npm view netpay3ds dist-tags --json
```

## Installation / Instalación

El nombre público del paquete continúa siendo siempre `netpay3ds`; no existen
paquetes separados por generación.

### Latest

```bash
npm install netpay3ds
```

es equivalente a:

```bash
npm install netpay3ds@latest
```

`latest` significa la última versión estable publicada y apunta a `2.0.0`. Es
la opción recomendada para nuevas integraciones Angular 22. El canal puede
avanzar, por lo que conviene fijar una versión exacta cuando se requiera
reproducibilidad absoluta.

### Legacy

```bash
npm install netpay3ds@legacy
```

`legacy` es la **línea de compatibilidad mantenida** para Angular 13.2–21 y
apunta actualmente a `1.1.1`. El término no significa que la librería esté
descontinuada: identifica la última versión estable soportada de 1.x. El canal
puede avanzar a futuras correcciones 1.x sin cruzar automáticamente a 2.x.

### Next

```bash
npm install netpay3ds@next
```

`next` se reserva para testing y validación prerelease con Angular 22. Puede
seguir apuntando a `2.0.0-next.1` hasta que exista un nuevo prerelease, pero no
lo uses como canal productivo ni como sustituto de `latest`.

### Specific version / Versión específica

Una versión exacta no avanza cuando cambia un dist-tag:

```bash
npm install netpay3ds@<exact-version>
npm install netpay3ds@1.1.1
npm install netpay3ds@2.0.0
```

Por ejemplo, `netpay3ds@legacy` puede avanzar dentro de 1.x, mientras
`netpay3ds@1.1.1` fija ese release. De forma equivalente, `latest` puede
avanzar entre releases estables, pero una versión exacta permanece fija. No
modifiques manualmente archivos dentro de `node_modules`.

No se necesitan `--force` ni `--legacy-peer-deps`. Angular y RxJS son peer
dependencies: la aplicación consumidora aporta versiones compatibles.

## Upgrading from 1.x to 2.x

Los cuatro exports históricos, las diez firmas públicas de
`Netpay3dsService`, `Netpay3dsModule` y el flujo observable de integración 3DS
se mantienen compatibles entre 1.x y 2.x. La migración 2.x actualiza
principalmente la compatibilidad con Angular 22; no requiere cambiar imports,
métodos o callbacks del comercio.

Si tu aplicación continúa en Angular 13.2–21, permanece en `legacy`/1.x. Cuando
la aplicación migre a Angular 22, valida primero 2.x con tus flujos
frictionless, challenge, errores y confirmación idempotente. No instales 2.x en
Angular 13–21 usando flags para omitir peer dependencies.

## Quick Start

El flujo completo tiene tres integraciones: tokenizar la tarjeta, ejecutar el
flujo 3DS y procesar/confirmar el pago. Consulta también la documentación de
[tokenización de tarjeta](https://docs.netpay.com.mx/v1.2.1/reference/tokenizacion-de-tarjeta)
y de [procesamiento del pago](https://docs.netpay.com.mx/v5.0/reference/3-procesar-pago-2).

## TypeScript

Primero, instala el canal o la versión que corresponda a tu Angular según las
secciones anteriores. Ejemplos actuales:

```bash
# Angular 13.2–21
npm install netpay3ds@legacy --save

# Angular 22
npm install netpay3ds@latest --save
```

Después integra el flujo:

1. Crea el token de tarjeta utilizando la librería `netpayjs`. Consulta
   [Tokenizar tarjeta](https://docs.netpay.com.mx/v1.2.1/reference/tokenizacion-de-tarjeta).

2. Importa `Netpay3dsModule` una sola vez en `app.module.ts`:

```ts
import { NgModule } from '@angular/core';
import { Netpay3dsModule } from 'netpay3ds';

@NgModule({
  imports: [
    Netpay3dsModule
  ]
})
export class AppModule {}
```

En `app.component.ts`, importa e inyecta el servicio:

```ts
import { Netpay3dsService } from 'netpay3ds';

constructor(private netpay3ds: Netpay3dsService) {}
```

Selecciona el ambiente antes de iniciar una operación y espera el callback de
`init` antes de llamar `config`:

```ts
const operation = this;
this.netpay3ds.setSandboxMode(true);
this.netpay3ds.init(() => {
  operation.netpay3ds.config(operation, amount, operation.callback);
});

callback(operation: any, referenceId: string | null): void {
  console.log('referenceId: ' + referenceId);
  if (referenceId) {
    operation.charges(referenceId);
  }
}

charges(referenceId: string): void {
  console.log('charges: ' + referenceId);
}
```

`init` obtiene la configuración 3DS, carga Songbird con SRI y comprueba que el
objeto Cardinal tenga los métodos requeridos. Si la inicialización falla, el
callback no se ejecuta y se registra un código controlado en `console.error`.

Toma el valor de `referenceId` para realizar el charge.

3. Realiza el [charge](https://docs.netpay.com.mx/v5.0/reference/3-procesar-pago-2)
   acorde con la documentación de NetPay.

4. Con la información de la respuesta del charge, ejecuta `canProceed`. Si
   devuelve `true`, llama `proceed` y espera su callback antes de confirmar:

```ts
const status = charge.status;
const responseCode = charge.threeDSecureResponse.responseCode;
const acsUrl = charge.threeDSecureResponse.acsUrl;
const paReq = charge.threeDSecureResponse.paReq;
const authenticationTransactionID =
  charge.threeDSecureResponse.authenticationTransactionID;

const canProceed = this.netpay3ds.canProceed(status, responseCode, acsUrl);
if (canProceed) {
  this.netpay3ds.proceed(
    this,
    acsUrl,
    paReq,
    authenticationTransactionID,
    this.callbackProceed
  );
} else {
  this.confirm(null);
}

callbackProceed(
  operation: any,
  processorTransactionId: string | null,
  status: string
): void {
  console.log('processorTransactionId: ' + processorTransactionId);
  if (status === 'success' && processorTransactionId) {
    operation.confirm(processorTransactionId);
  } else {
    // Rechazar la transacción y mostrar el mensaje correspondiente.
    console.log('error');
  }
}

confirm(processorTransactionId: string | null): void {
  console.log('confirm: ' + processorTransactionId);
}
```

5. Ejecuta el servicio de confirm acorde con la documentación. `netpay3ds`
   entrega el resultado 3DS al consumer, pero no ejecuta el endpoint de confirm
   ni el charge de la aplicación.

## JavaScript

Si deseas utilizar la distribución JavaScript, conserva los pasos 1, 3 y 5 y
reemplaza los pasos 2 y 4 por los siguientes.

6. Carga la librería correspondiente al ambiente.

Sandbox:

```html
<script src="https://cdn.netpay.mx/js/dev/netpay3ds.js"></script>
```

Live:

```html
<script src="https://cdn.netpay.mx/js/latest/netpay3ds.js"></script>
```

Inicializa y configura 3DS:

```js
const operation = this;
netpay3ds.setSandboxMode(true);
netpay3ds.init(function () {
  netpay3ds.config(operation, amount, callback);
});

const callback = function (sameOperation, referenceId) {
  console.log('referenceId: ' + referenceId);
  if (referenceId) {
    charges(referenceId);
  }
};

const charges = function (referenceId) {
  console.log('charges: ' + referenceId);
};
```

7. Evalúa la respuesta del charge y continúa el challenge cuando corresponda:

```js
const canProceed = netpay3ds.canProceed(status, responseCode, acsUrl);
console.log(canProceed);

if (canProceed) {
  netpay3ds.proceed(
    operation,
    acsUrl,
    paReq,
    authenticationTransactionID,
    callbackProceed
  );
} else {
  confirm(null);
}

const callbackProceed = function (sameOperation, processorTransactionId, status) {
  console.log('processorTransactionId: ' + processorTransactionId);
  if (status === 'success' && processorTransactionId) {
    confirm(processorTransactionId);
  } else {
    // Rechazar la transacción y mostrar el mensaje correspondiente.
    console.log('error');
  }
};

const confirm = function (processorTransactionId) {
  console.log('confirm: ' + processorTransactionId);
};
```

Si tienes conflictos con jQuery, puedes utilizar la distribución
`netpay3ds-noConflict`.

Sandbox:

```html
<script src="https://cdn.netpay.mx/js/dev/netpay3ds-noConflict.js"></script>
```

Live:

```html
<script src="https://cdn.netpay.mx/js/latest/netpay3ds-noConflict.js"></script>
```

## Environment Configuration

### Production

Production es el ambiente predeterminado. Si no se llama `setSandboxMode`, o
si se pasa cualquier valor distinto del booleano `true`, las operaciones usan:

```text
Backend:  https://suite.netpay.com.mx
Songbird: static.client.cardinaltrusted.com
```

### Sandbox

Sólo esta llamada activa Sandbox:

```ts
this.netpay3ds.setSandboxMode(true);
```

Sandbox usa:

```text
Backend:  https://gateway-154.netpaydev.com
Songbird: cas.static.client.cardinaltrusted.com
```

### setSandboxMode

El comportamiento es estricto y equivale conceptualmente a
`sandboxMode = value === true`:

| Valor | Ambiente resultante |
| --- | --- |
| `true` | Sandbox |
| `false`, `"true"`, `"false"`, `1`, `0` | Production |
| `null`, `undefined`, `{}`, `[]` | Production |
| No llamar el método | Production |

No hay coerción de strings o números y esos valores no producen una excepción.
Cada operación toma un snapshot del ambiente; cambiar el modo afecta las
operaciones nuevas, no una operación 3DS que ya está en curso.

`setUrl(url)` se conserva por compatibilidad para reemplazar el backend de
Sandbox en entornos controlados. Production siempre usa su URL definida por la
librería. No lo uses para omitir la validación del host de Songbird.

## Dynamic 3DS Configuration

1.x ya no depende de una URL o un hash de Songbird hardcodeados. Antes de
insertar el script realiza la siguiente solicitud en el ambiente seleccionado:

```text
GET <environment-base-url>/gateway-ecommerce/v3/3ds/config
```

La solicitud es un `GET` simple, sin body, credenciales ni headers
personalizados. Esto conserva el contrato CORS publicado por los gateways y no
requiere preflight.

El backend responde con:

```ts
{
  version: string;
  url: string;
  integrity: string;
  crossOrigin: 'anonymous' | 'use-credentials';
  releaseDate?: string;
}
```

La librería valida HTTPS, host, puerto, credenciales, ruta de la versión,
query/hash, `integrity`, `crossOrigin` y `releaseDate` antes de cargar cualquier
script. Una URL de Sandbox en una operación Production —o viceversa— se rechaza
con `THREEDS_ENVIRONMENT_MISMATCH`.

### Songbird

Songbird se carga desde la `url` validada y se reutiliza sólo cuando el script,
la versión, SRI y el ambiente coinciden. Las cargas concurrentes del mismo
ambiente se deduplican. Si una carga falla, el elemento se elimina para permitir
un reintento posterior.

La aplicación puede necesitar autorizar estos orígenes en su Content Security
Policy:

```text
Production:
connect-src https://suite.netpay.com.mx
script-src  https://static.client.cardinaltrusted.com

Sandbox:
connect-src https://gateway-154.netpaydev.com
script-src  https://cas.static.client.cardinaltrusted.com
```

Los orígenes de `frame-src` para challenge dependen de los ACS participantes y
deben definirse conforme a la política de la aplicación. Este cambio no requiere
`unsafe-eval` ni wildcards.

### Integrity / SRI

`integrity` es el hash Subresource Integrity SHA-384 entregado por el backend.
La librería lo asigna al elemento `<script>` junto con `crossOrigin`; no existe
un fallback que cargue Songbird sin SRI. Un hash ausente, mal formado o que no
coincida con el recurso detiene la carga de forma controlada.

El backend 3DS debe entregar juntos la URL y su hash correspondiente. Cuando
Visa/Cardinal publique una versión nueva de Songbird, el backend actualiza
`url`, `version`, `integrity`, `crossOrigin` y `releaseDate`; el frontend no
necesita un release sólo para sustituir esos valores.

### Cardinal

Una vez cargado Songbird, la librería valida y usa `Cardinal.configure`,
`Cardinal.setup`, `Cardinal.on` y `Cardinal.continue`. Durante `config`, solicita
el JWT Cardinal a `/gateway-ecommerce/v3/cardinal-auth`, registra
`payments.setupComplete` antes de invocar `Cardinal.setup` y devuelve el
`ReferenceId` por callback.

## 3DS Authentication Flow

### Frictionless

1. Inicializa la librería y llama `config(context, amount, callback)`.
2. Usa el `referenceId` para procesar el charge en el backend de tu aplicación.
3. Evalúa la respuesta con `canProceed(status, responseCode, acsUrl)`.
4. Si no se requiere `review`/ACS, no llames `proceed`; continúa el flujo
   frictionless de tu backend y ejecuta confirm una sola vez cuando corresponda.

`config` entrega `referenceId = null` cuando la configuración, Songbird, el JWT
o Cardinal setup fallan. No proceses el charge como si 3DS hubiera sido
configurado en ese caso.

### Review

`canProceed` devuelve `true` cuando `status === "review"` o cuando el backend
entrega la acción `PROCEED_TO_VALIDATE_AUTHENTICATION`, siempre que `acsUrl` sea
un valor no vacío. El parámetro `responseCode` se conserva por compatibilidad
con 0.5.x, aunque la decisión actual se basa en `status` y `acsUrl`.

### Challenge

Cuando `canProceed` sea `true`, llama:

```ts
this.netpay3ds.proceed(
  context,
  acsUrl,
  paReq,
  authenticationTransactionID,
  (sameContext, processorTransactionId, status) => {
    // El callback ocurre después de payments.validated.
  }
);
```

La librería registra el listener de `payments.validated` antes de ejecutar
`Cardinal.continue('cca', ...)`. Cardinal puede completar sin UI o presentar el
challenge del emisor; ambos caminos terminan por el mismo evento.

### payments.validated

El callback de `proceed` se ejecuta una sola vez cuando llega
`payments.validated`. Devuelve `status = "success"` si existe
`Payment.ProcessorTransactionId`; en caso contrario devuelve
`processorTransactionId = null` y `status = "error"`. Una falla o timeout de
Cardinal también termina con el callback de error controlado.

### Confirm

`confirm` no es un método público de `Netpay3dsService`. La aplicación debe
invocar su endpoint de confirmación después del resultado 3DS:

- frictionless: confirma según la respuesta aprobada del charge, sin llamar
  `proceed`;
- challenge/review: espera el callback posterior a `payments.validated` y
  confirma con el `processorTransactionId` exitoso;
- error: no confirmes como transacción autenticada.

Implementa idempotencia para que cada transacción se confirme una sola vez. El
consumer oficial incluye un adaptador `confirmOnce` y pruebas que impiden doble
confirm.

## Timeouts

Las etapas tienen límites independientes para que una falla no deje pendiente
el flujo completo:

| Etapa | Alcance |
| --- | --- |
| Config timeout | Solicitud de configuración dinámica 3DS |
| Songbird load timeout | Descarga y disponibilidad del objeto Cardinal |
| Backend auth timeout | Solicitud del JWT para Cardinal |
| Cardinal setup timeout | Espera de `payments.setupComplete` |
| Cardinal auth timeout | Espera de `payments.validated`, incluyendo challenge |
| Confirm timeout | Debe controlarse de forma independiente en la integración de confirm; el sample demuestra esta separación |

Los valores son detalles internos y pueden evolucionar; la aplicación no debe
sincronizar timers propios con constantes no exportadas. Un challenge tiene un
timeout mayor e independiente de los timeouts técnicos cortos.

## Error Handling

La compatibilidad de callbacks se conserva:

- `config` y `setup` entregan `referenceId = null` al fallar;
- `proceed` entrega `processorTransactionId = null` y `status = "error"`;
- `init` no llama su callback si Songbird no pudo inicializarse;
- excepciones lanzadas por callbacks del consumer se contienen y registran.

Los siguientes códigos existen en la implementación y pueden aparecer en
`console.error`. No constituyen un enum exportado ni sustituyen el manejo del
callback:

| Código | Significado |
| --- | --- |
| `THREEDS_BROWSER_ENVIRONMENT_REQUIRED` | La operación necesita un browser/DOM |
| `THREEDS_CONFIG_TIMEOUT` | Expiró la solicitud de configuración |
| `THREEDS_CONFIG_INVALID_JSON` | El backend respondió JSON inválido |
| `THREEDS_CONFIG_HTTP_ERROR` | El backend respondió un error HTTP |
| `THREEDS_CONFIG_REQUEST_FAILED` | Falló red, CSP, DNS, TLS o CORS |
| `THREEDS_CONFIG_INVALID` | URL, versión, SRI, crossOrigin o fecha inválidos |
| `THREEDS_ENVIRONMENT_MISMATCH` | La configuración/script no corresponde al ambiente |
| `THREEDS_SONGBIRD_LOAD_FAILED` | El navegador no pudo cargar o insertar Songbird |
| `THREEDS_SONGBIRD_TIMEOUT` | Expiró la carga de Songbird |
| `THREEDS_CARDINAL_NOT_AVAILABLE` | Cardinal o un método requerido no está disponible |
| `THREEDS_BACKEND_TIMEOUT` | Expiró la solicitud de autenticación backend |
| `THREEDS_BACKEND_REQUEST_FAILED` | Falló la solicitud del JWT Cardinal |
| `THREEDS_BACKEND_RESPONSE_INVALID` | El JWT o `ReferenceId` no son válidos |
| `THREEDS_CARDINAL_SETUP_TIMEOUT` | No llegó `payments.setupComplete` |
| `THREEDS_CARDINAL_AUTH_TIMEOUT` | No llegó `payments.validated` |
| `THREEDS_UNKNOWN_ERROR` | Error no clasificado por la capa 3DS |

Después de una falla de carga/configuración es válido iniciar una operación
nueva; no reutilices datos parciales de la operación fallida.

## API Reference

El tarball exporta cuatro entry points históricos:

```ts
Netpay3dsService
Netpay3dsComponent
Netpay3dsModule
TestViewComponent
```

`Netpay3dsService` conserva estos diez métodos públicos:

| Método | Contrato |
| --- | --- |
| `ngOnInit()` | No-op conservado por compatibilidad |
| `init(callback)` | Carga configuración/Songbird y llama el callback al quedar listo |
| `validate(successCallback)` | Wrapper legacy que registra `window.songbird.onload` |
| `setUrl(url)` | Reemplaza la base URL de Sandbox para operaciones nuevas |
| `setSandboxMode(flag)` | Activa Sandbox sólo cuando `flag === true` |
| `config(context, amount, callback)` | Obtiene JWT, configura Cardinal y devuelve `ReferenceId` |
| `setup(context, data, callback)` | Configura Cardinal con un objeto `{ jwt: string }` ya obtenido |
| `proceed(context, acsUrl, paReq, authenticationTransactionID, callback)` | Ejecuta autenticación y espera `payments.validated` |
| `canProceed(status, responseCode, acsUrl)` | Indica si la respuesta requiere `proceed` |
| `test()` | Método diagnóstico legacy que muestra `Hello World!` |

`context` es devuelto al callback y también asocia la operación con su snapshot
de ambiente. Usa el mismo objeto en `config`/`setup` y `proceed`.

## Complete Example

El ejemplo siguiente usa únicamente la API pública real de `netpay3ds`.
`/api/charges` y `/api/confirm` representan endpoints propios del consumer y
deben adaptarse a su contrato de pagos.

```ts
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { Netpay3dsService } from 'netpay3ds';

interface ChargeResponse {
  status: string;
  threeDSecureResponse: {
    responseCode: number;
    acsUrl: string;
    paReq: string;
    authenticationTransactionID: string;
  };
}

@Component({ selector: 'app-payment', template: '' })
export class PaymentComponent {
  private readonly confirmed = new Set<string>();

  constructor(
    private readonly netpay3ds: Netpay3dsService,
    private readonly http: HttpClient
  ) {}

  pay(amount: number, transactionTokenId: string): void {
    const operation = { transactionTokenId };

    // Omite esta llamada para Production; Production es el default.
    this.netpay3ds.setSandboxMode(true);
    this.netpay3ds.init(() => {
      this.netpay3ds.config(operation, amount,
        (_operation: object, referenceId: string | null) => {
          if (!referenceId) return;

          this.http.post<ChargeResponse>('/api/charges', {
            transactionTokenId,
            amount,
            referenceId
          }).subscribe(charge => this.handleCharge(operation, charge));
        });
    });
  }

  private handleCharge(operation: { transactionTokenId: string }, charge: ChargeResponse): void {
    const threeDS = charge.threeDSecureResponse;
    if (!this.netpay3ds.canProceed(charge.status, threeDS.responseCode, threeDS.acsUrl)) {
      this.confirmOnce(operation.transactionTokenId, null);
      return;
    }

    this.netpay3ds.proceed(
      operation,
      threeDS.acsUrl,
      threeDS.paReq,
      threeDS.authenticationTransactionID,
      (_operation: object, processorTransactionId: string | null, status: string) => {
        if (status === 'success' && processorTransactionId) {
          this.confirmOnce(operation.transactionTokenId, processorTransactionId);
        }
      }
    );
  }

  private confirmOnce(transactionTokenId: string, processorTransactionId: string | null): void {
    if (this.confirmed.has(transactionTokenId)) return;
    this.confirmed.add(transactionTokenId);
    this.http.post('/api/confirm', {
      transactionTokenId,
      processorTransactionId
    }).subscribe();
  }
}
```

En producción, la idempotencia también debe aplicarse en el backend; un `Set`
en memoria sólo ilustra el orden y no sustituye una llave idempotente durable.

## Migration from 0.5.x to 1.x

- El nombre del paquete sigue siendo `netpay3ds`.
- Se conservan los cuatro exports y diez métodos públicos de 0.5.7.
- Una integración dentro de los peer ranges no requiere cambios obligatorios
  de imports por este release.
- La configuración de Songbird ahora se obtiene dinámicamente del backend.
- La URL y el SRI se validan juntos; no hay fallback inseguro.
- Production sigue siendo el ambiente por defecto y sólo `true` activa Sandbox.
- Verifica CORS/CSP y que el backend entregue el host correcto por ambiente.
- Prueba los caminos frictionless, review/challenge, error, timeout y confirm
  idempotente antes de hacer rollout.

No se garantiza impacto cero: valida la aplicación real y su backend antes de
promover 1.x. Los archivos `MIGRATION.md` y `CHANGELOG.md`, incluidos en el
paquete, contienen la estrategia completa y el detalle por versión.

## Versioning

- `1.x`: línea Legacy / Maintenance compatible con consumers Angular 13.2–21;
  la versión publicada actual es `1.1.1`.
- `2.x`: línea moderna para consumers Angular 22; la versión estable actual es
  `2.0.0`.

`latest` apunta a `2.0.0` y `legacy` conserva `1.1.1`. `next` queda reservado
para prereleases futuros de 2.x y puede continuar señalando el último
prerelease publicado. Un prerelease o RC nunca debe mover `latest`.

### Legacy Support

Los consumers pueden fijar una versión exacta o una línea:

```json
{
  "dependencies": {
    "netpay3ds": "^1.1.1"
  }
}
```

`^1.1.1` permanece en 1.x. Una dependencia exacta `0.5.7`, `^0.5.7` o
`~0.5.7` no cruza automáticamente a 1.x. La fecha EOL de 1.x permanece pendiente
de definición corporativa.

El archivo `RELEASING.md`, incluido en el paquete, describe el proceso
reproducible de release.

## License

Distribuido bajo la licencia [MIT](https://opensource.org/license/mit).

## Further Help

Para soporte de integración, escribe a `soporte@netpay.com.mx`.
