From 930baa4a60ea4796922597a7c06f3a82b2a528bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Devos?= Date: Mon, 5 Oct 2026 09:53:37 +0200 Subject: [PATCH] fix: sync generated types and bump undici - sync dropin and headless-checkout references (#97, #98) - sync securefields-js-sdk references 1.8.0 (#100) - bump undici from 8.10.0 to 8.11.2 (#101) - add lit, lit-html, final-form and zod as dependencies: the published dropin/headless-checkout types import them, so consumers need them installed for the types to resolve (type-only, bundle size unchanged) --- package-lock.json | 98 ++- package.json | 8 +- src/generated/types/dropin.d.ts | 817 +++++++++++++++++---- src/generated/types/headless-checkout.d.ts | 703 ++++++++++++++++-- src/generated/types/securefields.d.ts | 284 +++++-- 5 files changed, 1653 insertions(+), 257 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f470f4..6f88daa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,12 @@ "name": "@purse-eu/web-sdk", "version": "0.10.0", "license": "MIT", + "dependencies": { + "final-form": "^4.20.10", + "lit": "^3.3.3", + "lit-html": "^3.3.3", + "zod": "^3.25.76" + }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", "@eslint/js": "^10.0.1", @@ -126,6 +132,15 @@ "node": "^22.13.0 || >=24.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@braidai/lang": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", @@ -559,6 +574,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", + "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, "node_modules/@loaderkit/resolve": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.6.tgz", @@ -947,6 +977,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -2031,6 +2067,22 @@ "node": ">=16.0.0" } }, + "node_modules/final-form": { + "version": "4.20.10", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", + "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2604,6 +2656,37 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/local-pkg": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", @@ -3534,9 +3617,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", "dev": true, "license": "MIT", "engines": { @@ -4028,6 +4111,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d263499..0bb06d8 100644 --- a/package.json +++ b/package.json @@ -50,5 +50,11 @@ "dist" ], "author": "@upstreampay", - "license": "MIT" + "license": "MIT", + "dependencies": { + "final-form": "^4.20.10", + "lit": "^3.3.3", + "lit-html": "^3.3.3", + "zod": "^3.25.76" + } } diff --git a/src/generated/types/dropin.d.ts b/src/generated/types/dropin.d.ts index 90e0e78..72ab1bd 100644 --- a/src/generated/types/dropin.d.ts +++ b/src/generated/types/dropin.d.ts @@ -1,6 +1,8 @@ // Generated by dts-bundle-generator v9.5.1 import { AnyObject } from 'final-form'; +import { CSSResult, LitElement, PropertyValues, TemplateResult as TemplateResult_2, nothing } from 'lit'; +import { TemplateResult } from 'lit-html'; import { ZodIssue, z } from 'zod'; declare const Environments: readonly [ @@ -35,6 +37,7 @@ declare const Partners: { readonly illicado: "illicado"; readonly ingenico: "ingenico"; readonly klarna: "klarna"; + readonly lyra: "lyra"; readonly mercanet: "mercanet"; readonly oney: "oney"; readonly oneycard: "oneycard"; @@ -47,6 +50,7 @@ declare const Partners: { readonly riverty: "riverty"; readonly scalapay: "scalapay"; readonly scellius: "scellius"; + readonly sequra: "sequra"; readonly sipsv2: "sipsv2"; readonly slimpay: "slimpay"; readonly sogecommerce: "sogecommerce"; @@ -151,6 +155,14 @@ export type IntegratedToken = { source?: "MERCHANT" | "SHARED_WALLET"; savable_to_shared_wallet?: boolean; created_at?: string; + /** + * Network-token counterpart of this card token. When `available` is true and + * the experience enables network tokens (`settings.network_token`), the + * payment goes through the network token and no CVV is collected. + */ + network_token?: { + available?: boolean; + }; }; export interface MethodConfiguration { stable: boolean; @@ -166,12 +178,25 @@ export interface MethodConfiguration { settings: { registration_mode?: string; save_token?: boolean; + save_token_mode?: "ALWAYS" | "PREFER" | "PREFER_NO" | "NEVER"; direct_debit?: boolean; + /** + * Front-settings gate for network tokens. Tokens carrying an available + * `network_token` only pay through it (and hide their CVV) when this is + * true. + */ + network_token?: boolean; payment_min_amount?: number; payment_max_amount?: number; usage_per_session_limit?: number; merchant_specifics?: { integration_mode?: "redirection" | "popup"; + /** + * When truthy, the CVV field is hidden and not required when paying with + * a saved token. Configured per experience in the merchant back-office, + * so the value can reach the front as a string. + */ + pay_token_no_cvv?: boolean | "true" | "false"; }; }; } @@ -350,6 +375,11 @@ declare const PurseErrors: { message: string; documentationLink: string; }; + readonly API_UNKNOWN_ERROR: { + code: string; + message: string; + documentationLink: string; + }; }; export type PurseErrorCodes = keyof typeof PurseErrors; export type JsonSerializable = string | number | boolean | null | { @@ -449,7 +479,10 @@ export type ItemValidationData> = { partner: string; session_id: string; transaction_id: string; - plugin_result: Result; + plugin_result: Result & { + status?: string; + amount?: number; + }; status: { action: TransactionAction; state: StatusState; @@ -514,19 +547,33 @@ export interface LoanSimulation { export type WalletItem = { id: string; name?: string; + favorite?: boolean; }; declare class ApiError extends PurseError { readonly path: string; readonly error: string; responseHeaders?: Record; + /** HTTP status of the response that produced this error, when available (e.g. 400, 429, 500). */ + status?: number; constructor(error: (typeof PurseErrors)[PurseErrorCodes], path?: string, errorCode?: string, message?: string); } +/** + * Which product an SDK is embedded in, when it can be embedded by another. + * + * `direct` means the merchant integrated it themselves. Always sent explicitly so a + * missing value means "SDK build predates wrapper reporting" rather than "direct". + * + * @internal Not part of the public integration surface — set by the wrapping product. + */ +export type MonitoringWrapper = "direct" | "dropin"; export interface MonitoringApiEventMessage { instanceId?: string; entityId?: string; paymentSessionId?: string; walletSessionId?: string; version?: string; + /** @internal See {@link MonitoringWrapper}. */ + wrapper?: MonitoringWrapper; env?: EnvironmentTarget; event: Event; } @@ -556,11 +603,12 @@ export interface WalletAPI { deleteAllTokens(): Promise; setAsFavorite(data: { id: string; + favorite: boolean; }): Promise; } export interface MonitoringAPI { getApiKey(): string; - patchExtrasData(E: Pick, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version">): void; + patchExtrasData(E: Pick, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version" | "wrapper">): void; sendLogs(E: E[]): void; } export type APIPaths = { @@ -576,7 +624,7 @@ declare class FetchMonitoringAPI implements MonitoringAPI { private extras; constructor(env: EnvironmentTarget, root: string, apiKey: string); getApiKey(): string; - patchExtrasData: (payload: Partial, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version">>) => void; + patchExtrasData: (payload: Partial, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version" | "wrapper">>) => void; sendLogs: (events: Event[]) => void; } export type Subscriber = (value: T) => void; @@ -693,6 +741,42 @@ export declare interface CreditCardValidateUiResult { export declare type DeepPartial = T extends any[] ? T : { [P in keyof T]?: DeepPartial; }; +export declare type EnvSource = { + id: string; + singleton?: boolean; + production: Source; +} & { + [key in ExternalSourceEnv]?: Source; +}; +export declare type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]; +declare const ErrorCodes: { + readonly MethodIsNotImplementedByPlugin: "MethodIsNotImplementedByPlugin"; + readonly PluginEncounteredALethalException: "PluginEncounteredALethalException"; + readonly PluginIsIncompatibleWithClient: "PluginIsIncompatibleWithClient"; + readonly PreValidationIsntAvailableForPayButton: "PreValidationIsntAvailableForPayButton"; + readonly PostValidationDataIsMissingInformations: "PostValidationDataIsMissingInformations"; + readonly CannotInitWithoutSession: "CannotInitWithoutSession"; + readonly CannotSubmitWithoutClient: "CannotSubmitWithoutClient"; + readonly CannotPreValidateWithoutPlugin: "CannotPreValidateWithoutPlugin"; + readonly FormValidationFailed: "FormValidationFailed"; + readonly ClientNotFoundWhenSettingSession: "ClientNotFoundWhenSettingSession"; + readonly ClientNotFoundAfterSourceLoad: "ClientNotFoundAfterSourceLoad"; + readonly PreValidationFailed: "PreValidationFailed"; + readonly PostValidationFailed: "PostValidationFailed"; + readonly DynamicAmountNotInSessionRange: "DynamicAmountNotInSessionRange"; + readonly CantSetBrandThatIsNotAmongMatches: "CantSetBrandThatIsNotAmongMatches"; + readonly CannotPostValidateWithoutPlugin: "CannotPostValidateWithoutPlugin"; + readonly MissingDataFromContext: "MissingDataFromContext"; + readonly "3DSCantProceedWithoutRedirectionURL": "3DSCantProceedWithoutRedirectionURL"; + readonly "3DSFingerprintingFailed": "3DSFingerprintingFailed"; + readonly APIResultUnsatisfying: "APIResultUnsatisfying"; + readonly PluginInitFailed: "PluginInitFailed"; + readonly TargetNotInDocument: "TargetNotInDocument"; + readonly MissingTargetFromConfig: "MissingTargetFromConfig"; + readonly MultipleMatchesForTargetInDocument: "MultipleMatchesForTargetInDocument"; + readonly PartnerDoesNotSupportUI: "PartnerDoesNotSupportUI"; + readonly HolderNameFieldRequired: "HolderNameFieldRequired"; +}; export declare type ErrorPayload = { reason?: string; } & Record; @@ -888,6 +972,25 @@ declare const Events: { "partnerError" ]; }; + /** + * The plugin's own integration broke for this attempt — a dead popup, a + * severed window transport — and the failure will not clear by retrying, but + * it is not permanent either: a fresh amount or session can make the method + * work again. + * + * Distinct from `partnerError`, which also carries ordinary payment outcomes + * (Lyra reports declined cards through it). Sharing both topics means every + * existing `error`/`partnerError` subscriber keeps receiving it; what the host + * does about it — deactivating the method, and when to lift that — is the + * host's policy, not the plugin's. + */ + readonly partnerRecoverableError: { + readonly code: "partnerRecoverableError"; + readonly topics: readonly [ + "error", + "partnerError" + ]; + }; readonly change: { readonly code: "change"; readonly topics: readonly [ @@ -955,9 +1058,10 @@ declare const Events: { ]; }; }; +export declare type ExternalSourceEnv = "production" | "sandbox" | "test" | "development"; export declare type FieldValidationError = (typeof FieldValidationErrors)[keyof typeof FieldValidationErrors]; declare const FieldValidationErrors: { - readonly cannotBeEmpty: "cannotBeEmpty"; + readonly required: "required"; readonly dateOutOfRange: "dateOutOfRange"; readonly format: "format"; readonly lengthOutOfRange: "lengthOutOfRange"; @@ -1063,19 +1167,24 @@ export declare interface HostedFieldsUIOptions { * Some partners displays a complete form and let you configure the text through a JS configuration. We've mapped those * texts and let you override all off them form this single object. During a session, while displaying a form some of * the variables might not be used, it depends on the context. + * + * Every field is plain text and is rendered as such. The only exceptions are `consentText` and + * `loanSimulation`, which accept limited HTML — see their own docs. */ export declare interface HostedFormUIOptions { panPlaceholder?: string; panInputLabel?: string; panRequiredError?: string; - panFormatError?: string; + /** @deprecated use panRequiredError */ panCannotBeEmptyError?: string; + panFormatError?: string; panTooltipText?: string; birthdatePlaceholder?: string; birthdateInputLabel?: string; birthdateRequiredError?: string; - birthdateFormatError?: string; + /** @deprecated use birthdateRequiredError */ birthdateCannotBeEmptyError?: string; + birthdateFormatError?: string; birthdateOutOfRangeError?: string; birthdateTooltipText?: string; cvvPlaceholder?: string; @@ -1086,6 +1195,9 @@ export declare interface HostedFormUIOptions { * This value is interpolated with {url:string} * use {{variable}} to use interpolation * @example 'Your will agree to the tos ' + * + * Accepts HTML, sanitised by allowlist: a, b, strong, em, i, u, span, br, p, ul, ol, li, + * with href/target/rel/style only. `href` must be an absolute https URL. */ consentText?: string; /** @@ -1097,8 +1209,9 @@ export declare interface HostedFormUIOptions { */ cvv4InputLabel?: string; cvvRequiredError?: string; - cvvFormatError?: string; + /** @deprecated use cvvRequiredError */ cvvCannotBeEmptyError?: string; + cvvFormatError?: string; cvvTooltipText?: string; /** * When true, the CVV field is hidden and not required for payment. @@ -1113,30 +1226,35 @@ export declare interface HostedFormUIOptions { expirationPlaceholder?: string; expirationInputLabel?: string; expirationRequiredError?: string; - expirationFormatError?: string; + /** @deprecated use expirationRequiredError */ expirationCannotBeEmptyError?: string; + expirationFormatError?: string; expirationOutOfRangeError?: string; expirationTooltipText?: string; holderPlaceholder?: string; holderInputLabel?: string; holderRequiredError?: string; - holderFormatError?: string; + /** @deprecated use holderRequiredError */ holderCannotBeEmptyError?: string; + holderFormatError?: string; ibanPlaceholder?: string; ibanInputLabel?: string; ibanRequiredError?: string; - ibanFormatError?: string; + /** @deprecated use ibanRequiredError */ ibanCannotBeEmptyError?: string; + ibanFormatError?: string; emailPlaceholder?: string; emailInputLabel?: string; emailRequiredError?: string; - emailFormatError?: string; + /** @deprecated use emailRequiredError */ emailCannotBeEmptyError?: string; + emailFormatError?: string; phonePlaceholder?: string; phoneInputLabel?: string; phoneRequiredError?: string; - phoneFormatError?: string; + /** @deprecated use phoneRequiredError */ phoneCannotBeEmptyError?: string; + phoneFormatError?: string; /** * This value is interpolated with: * - first @@ -1146,6 +1264,8 @@ export declare interface HostedFormUIOptions { * - cost * - taeg * use {{variable}} to use interpolation + * + * Accepts the same HTML allowlist as `consentText`. */ loanSimulation?: string; /** @@ -1165,16 +1285,13 @@ declare const IntegrationTypes: { readonly xPayButton: "xPayButton"; readonly redirection: "redirection"; }; -export declare interface KRError_2 { - errorCode: string; - errorMessage: string; - detailedErrorCode?: string; - detailedErrorMessage?: string; - field?: string; - children?: Array; - metadata?: Record; -} -export declare type ManagedHostedFieldValue = "valid" | Extract | null; +export declare type Interface = (typeof Interfaces)[keyof typeof Interfaces]; +declare const Interfaces: { + readonly form: "form"; + readonly hostedfields: "hostedfields"; + readonly payButton: "payButton"; +}; +export declare type ManagedHostedFieldValue = "valid" | Extract | null; /** * Options let you override theme configuration and translations on our UIs. */ @@ -1210,6 +1327,173 @@ export declare type Options = { */ locale?: string; }; +/** + * SDK-agnostic partner error contract. `events.ts` must not know any specific + * partner SDK's internals — the normalized `message` is the stable contract + * consumers read; the raw partner error is carried opaquely in `originalError`. + */ +export declare interface PartnerErrorPayload { + /** Normalized, contextual message. The stable contract consumers read. */ + message: string; + /** Raw partner SDK error. Opaque — debug only, never introspect its shape or cast it. */ + originalError?: unknown; +} +declare class PartnerUI> extends LitElement { + #private; + private sourceDownloaded; + private plugin; + private extraSources; + private data; + private readonly context; + session: LegacyPaymentSession | undefined; + env: APIEnv | null; + options: Options; + amount?: number; + register: boolean | undefined; + partner: Partner; + method: Method; + interface: Interface; + token?: Token; + /** + * A payzen xpay specific function to trigger onBeforeValidate hook outside the payment process + * This function should throw if the onBeforeValidate fails, allowing us to `return false` in the `onClick` of the smartForm + * Returning false in the onClick prevents to submit + */ + __xpay_payzen_specific_triggerOnBeforeValidate__: (method: Extract) => Promise; + constructor(); + private get envName(); + dispatchUnifiedEvent: (code: K, payload?: UnifiedEventPayload[K] | null) => void; + preValidation(): Promise; + postValidation(data: ValidationResultData, redirectionHandler?: RedirectionHandler): Promise; + /** + * Use this method to run the validation of the UI. It can be useful to trigger a validation of the UI, in a "onsubmit" validation workflow for instance. + * This should not be confused with the "validate" concept which is the API call to validate the payment. + * */ + validateUi(): Promise; + /** + * When detected brands are multiple, use this method to set the selected brand. + * default is detectedBrands[0] + * @param brand the brand to select + * @returns void + **/ + setSelectedBrand(brand: CardScheme): void; + connectedCallback(): void; + /** + * The element has no shadow root and is mounted into merchant-controlled DOM, so anything that + * serialises markup — session replay, error reporters, extension content scripts — captures + * attribute values. `session` alone carries every saved card in the session. These three stay + * supported as attribute *inputs* (`attribute: false` would silently starve callers that still + * pass them, breaking saved-card payments with no error), but nothing is left behind in the DOM. + * SDK-12163. + * + * Called from `connectedCallback`, which is where it is safe: custom-element upgrade replays + * `attributeChangedCallback` for existing attributes *before* `connectedCallback`, so Lit has + * already parsed them, and `connectedCallback` runs synchronously inside `appendChild`, so no + * script can read them in between. + * + * The value has to be re-assigned after removal: `removeAttribute` re-enters Lit's + * `attributeChangedCallback` with `null`, and the default `type: Object` converter maps that + * back to `null` — removing the attribute would otherwise wipe the property it just parsed. + * + * Only ever scrub inputs that callers pass by *property*. Svelte caches the attribute values it + * wrote on the node and skips a same-value rewrite, so an attribute removed behind its back + * never comes back — `amount`, `partner`, `method` and `interface` must stay out of here. + */ + private scrubSecretAttributes; + disconnectedCallback(): void; + performUpdate(): void; + private updateData; + private loadExtraSources; + private getSourceListener; + updated(changedProperties: PropertyValues): void; + willUpdate(changedProperties: PropertyValues): void; + createRenderRoot(): this; + render(): typeof nothing | TemplateResult<1>; +} +/** + * The partner ui context is the piece of data & abilities that we give to the plugins to operate with. + */ +export declare interface PartnerUIContext> { + /** + * The context provides a way to dispatch UnifiedEvent. This is required because our plugins is responsible for + * dispatching key events like fulfillment. + */ + dispatchEvent: (code: K, payload?: UnifiedEventPayload[K] | null) => void; + /** + * This is a reference to the partner ui root node, it is useful for plugins that need to append elements to the DOM + */ + shadowRoot: HTMLElement; + /** + * Data in the context is the "reactive" part, so you can read it through an accessor. + */ + data: () => Data; + /** + * This is the setter for the data. It takes a partial object of Data and patch the current state with it. + * If the references you passed changed the original ones, it will rerender the UI and also call the plugin's onDataChanged + */ + updateData: (data: Partial) => void; + /** + * The current payment session, can be undefined if no session is set. + */ + session?: LegacyPaymentSession; + /** + * This amount could be less than the session total, and plugins might want to perform action when it changes. + * This amount is less than the total when a secondary is involved for instance. + */ + amount?: number; + /** + * The redirection interface allows the plugin to gracefully redirect to another location. + */ + redirection: RedirectionHandler; + /** + * A helper to make calls to the API. We give it the API Root. + */ + api: PaymentAPI; + /** + * The env we're running the plugin in. This is useful for plugins that need to download extra sources. + */ + envName?: EnvironmentTarget; + /** + * Gives the ability to download extra sources at runtime + */ + loadExtraSources: (sources: EnvSource[]) => Promise; + /** + * If the partner UI renders a token, this will be set with the token extracted from session + */ + token?: Token; + /** + * The current partner ui requested interface + */ + ui: Interface; + /** + * The current partner ui's options + */ + options: Options; + /** + * Partner ui theme + */ + theme: Theme; + /** + * If the partners create some sort of client (Securefields, paypal...), its reference can be stored here + */ + client?: Client; + /** + * Current partner ui register attribute. + */ + register?: boolean; + /** + * This is a wild card that can be updated by the plugin to keep references without re-rendering the view + */ + dmz: { + initialStepId: number | null; + } & Record; + /** + * A payzen xpay specific function to trigger onBeforeValidate hook outside the payment process + * This function should throw if the onBeforeValidate fails, allowing us to `return false` in the `onClick` of the smartForm + * Returning false in the onClick prevents to submit + */ + __xpay_payzen_specific_triggerOnBeforeValidate__: (method: Extract) => Promise; +} /** * A theme is carrying all the style variables use across our UIs. * global can be seen as a default values store on which the other scopes will rely on to fall back a value. @@ -1324,6 +1608,13 @@ export declare type PartnerUITheme = { helperText: ThemeScope<"helperText">; tooltip: ThemeScope<"tooltip">; }; +declare class PursePartnerUI> extends PartnerUI { +} +export declare interface RedirectionHandler { + submitRedirectionForm(action: string, serializedFormData: string): Promise | void; + submitStringRedirectionForm(id: string, responseFormAsString: string): Promise | void; + redirectToUrl(url: string): Promise | void; +} export declare interface RedirectionUIOptions { title?: string; } @@ -1331,6 +1622,37 @@ export declare type Source = { type: "script" | "link"; attributes: Record; }; +declare class Theme { + static readonly UNEXPOSED: { + "background-color": string; + "brand-select-width": string; + "gap-small": string; + "gap-tiny": string; + "input-height": string; + "divider-margin": string; + "divider-bg": string; + }; + static readonly defaultTheme: PartnerUIOptionsTheme; + private _currentTheme; + private readonly _setProperty; + constructor(_setProperty: Theme["_setProperty"], theme?: PartnerUIOptionsTheme); + setTheme(theme?: PartnerUIOptionsTheme): void; + getValue(scope: Scope, key: keyof PartnerUITheme[Scope], variant?: ThemeScopesVariants): string | undefined; + getGlobalOverrides(): CSSResult; + applyCurrentTheme(): void; + private get currentTheme(); + private set currentTheme(value); + /** + * This method is in charge of creating a complete theme (not partial) using the default theme. + * if the `input['::placeholder']['color']` is not defined + * It will fall back to `input['color']` + * then to `global['::placeholder']['color']` + * and then to `global['color']` + * @param partialThemeFromOptions + * @private + */ + private scaffoldThemeValues; +} declare const THEME_SCOPES: readonly [ "global", "input", @@ -1432,6 +1754,16 @@ export declare type ThemeScopesVariants = ThemeSco export declare type ThemeScopesVariantsMap = { [K in ThemeScopeName]: (typeof THEME_SCOPES_VARIANTS)[K][number]; }; +export declare type Token = { + id: string; + expiration_date: string; + value: string; + selected_network: string; + holder_name: string; + description: { + display_token: string; + }; +}; export declare interface UnifiedEventPayload { clientError: ErrorPayload | null; clientNotFound: ErrorPayload | null; @@ -1484,9 +1816,13 @@ export declare interface UnifiedEventPayload { interface: string; fallBackInterface: string; }; + /** `code`/`reason` carry the rejecting `UnifiedError`, so the log says which check refused. */ pluginIsIncompatibleWithClient: { partner: Partner; method: Method; + code?: ErrorCode; + reason?: string; + stack?: string; }; pluginPostValidation: Record | null; pluginPreValidation: Record | null; @@ -1506,8 +1842,14 @@ export declare interface UnifiedEventPayload { } | null; other: Record | null; partnerError: { - error: Omit; + error: PartnerErrorPayload; + method: string; + partner: string; + }; + partnerRecoverableError: { + error: PartnerErrorPayload; method: string; + partner: string; }; change: HostedFieldsEventPayload | null; focus: HostedFieldsEventPayload | null; @@ -1542,16 +1884,45 @@ export declare interface XPayButtonUIOptions { * @example 'fr-FR' */ locale?: string; + /** + * Text preceding the Apple Pay logo, on partners rendering their own button label (e.g. Lyra smart form). + * Native Apple Pay buttons ignore it, they carry their own localized wording. + * Defaults to a locale-based translation. + * @example 'Buy with' + */ + label?: string; }; google?: GooglePayButtonOptions & { height?: number; + /** + * Text preceding the Google Pay logo, on partners rendering their own button label (e.g. Lyra smart form). + * Native Google Pay buttons ignore it, they carry their own localized wording. + * Defaults to a locale-based translation. + * @example 'Buy with' + */ + label?: string; }; paypal?: { + /** + * @deprecated Ignored since the migration to PayPal JS SDK v6: layout was a + * property of the v5 multi-button `paypal.Buttons()` container, and v6 renders + * exactly one button web component per `` element. Kept for + * source compatibility — remove on the next breaking release. + */ layout?: "vertical" | "horizontal"; color?: "silver" | "white" | "black" | "gold" | "blue"; - shape?: "rect" | "pill"; + shape?: "rect" | "pill" | "sharp"; height?: number; - label?: "paypal" | "checkout" | "buynow" | "pay" | "installment"; + /** + * Button wording. `paypal` renders the wordmark alone and is the default; + * `installment` was dropped by the PayPal JS SDK v6 and falls back to it. + */ + label?: "paypal" | "checkout" | "buynow" | "pay" | "subscribe" | "donate" | "installment"; + /** + * Corner radius of the button, in pixels. Takes precedence over `shape`. + * See https://developer.paypal.com/sdk/js/reference/#borderRadius + */ + borderRadius?: number; /** * When true, removes the max-width constraint applied by PayPal on the button container. * See https://developer.paypal.com/sdk/js/reference/#width @@ -1566,6 +1937,11 @@ export interface CardSchemeAsset { declare class ApiLogger extends FetchMonitoringAPI> { constructor(env: EnvironmentTarget, apiKey: string, apiRoot?: string); } +declare function buildPaymentElementAccessors(create: (inter: I) => I extends "form" ? PurseHeadlessCheckoutPaymentElement_2 : PurseHeadlessCheckoutHostedFields_2): { + getPaymentElement: (options?: Omit) => PurseHeadlessCheckoutPaymentElement_2; + getHostedFields: (options?: HostedFieldsOptions) => PurseHeadlessCheckoutHostedFields_2; + getElementInstance: () => PurseHeadlessCheckoutPaymentElement_2 | PurseHeadlessCheckoutHostedFields_2 | undefined; +}; /** * Represents the disabled state of a payment method. * @property code - {@link DisabledStateCode} The code indicating the reason for the disabled state. @@ -1590,6 +1966,7 @@ export declare type DisabledStatePayload = { PAYMENT_ELEMENT_RENDER_FAILED: never; PLUGIN_INCOMPATIBLE: never; PARTNER_UI_FATAL_ERROR: never; + PARTNER_UI_RECOVERABLE_ERROR: never; FATAL_ERROR_BEFORE_RENDER: never; MIN_AMOUNT_NOT_MET: { minAmount: number | null; @@ -1645,6 +2022,10 @@ export declare interface Payment { methods: Readable; tokens: Readable; walletTokens: Readable; + primaryMethodElementRegistry: Map; + registration: Writable; + }>; paymentSession: Readable; session: PaymentSession; walletSession: WalletSession | null; @@ -1865,9 +2246,11 @@ declare class PurseHeadlessCheckout { * - 'idle': The session is idle and not yet initialized. * - 'ready': The session is initialized and ready for payment method selection. * - 'submitting': The payment is being submitted. - * - 'submitted': The payment has been successfully submitted. + * - 'submitted': The session was validated successfully and is now spent. Further submissions + * are refused, since the API accepts one validation per session. * - 'expired': The session has expired. - * - 'error': An error occurred with the session. + * - 'error': An error occurred with the session, including a validation the API refused or a + * payment it declined. The session was not consumed, so submitting again is allowed. * @example * ```ts * headlessCheckout.sessionState.subscribe((state: PurseHeadlessCheckoutSessionState) => { @@ -1958,11 +2341,28 @@ declare class PurseHeadlessCheckout { */ setSession(widgetData: PurseHeadlessCheckoutV2Params): Promise; setSession(widgetData: PurseHeadlessCheckoutV1Params["paymentSession"]): Promise; + /** + * Shared submission path. Both entry points funnel through here — the public + * `submitPayment()` and express/wallet plugins requesting validation themselves. + * + * The `HEADLESS_CHECKOUT_SUBMIT_PAYMENT` event is emitted here rather than on the + * public method because `withMonitoringProxy` only sees calls made *through* the + * returned proxy. Internal callers hold a reference bound in the constructor, + * before the proxy exists, so monitoring attached to the public method never fires + * for them — which silently lost every wallet submission. + * + * It is emitted before the session and fulfilment guards on purpose: the event + * means "a submission was attempted", so guard failures stay visible. Pair it with + * `HEADLESS_CHECKOUT_PAYMENT_ERROR` to see which attempts were rejected. + */ private _submitPayment; /** * Submits the current payment configuration for processing. * This will validate and process all payment methods in the current split configuration. * + * Resolves without doing anything when a submission is already in flight, or when the session + * has already been validated successfully. Watch `sessionState` to tell the two apart. + * * @throws * - {@link PurseHeadlessCheckoutError} `MISSING_SESSION` \ * If the session is missing @@ -2071,6 +2471,67 @@ export declare interface PurseHeadlessCheckoutEditable { name: string; }): Promise; } +declare class PurseHeadlessCheckoutElement { + private readonly manuallyTriggerHook; + private readonly onSessionTerminated?; + protected readonly item: PurseHeadlessCheckoutPaymentItemBase; + protected readonly payment: Payment; + private fulfilled; + private _wasEverReady; + private readonly registration?; + private readonly amount?; + private readonly interface?; + private readonly env; + protected readonly method: Derived; + private storeListeners; + protected readonly options: Writable; + protected UIListeners: (() => void)[]; + private readonly eventsMap; + ui: PursePartnerUI | null; + constructor(item: PurseHeadlessCheckoutPaymentItemBase, data: { + payment: Payment; + env: APIEnv; + registration?: Readable; + amount?: Readable; + interface?: Interface; + }, manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { + actualMethod?: Method; + sourceId?: string; + }) => Promise, onSessionTerminated?: (() => void) | undefined); + getInterface(): Interface | undefined; + wasEverReady(): boolean; + remove: () => void; + setOptions(options?: Options): void; + hasUI(): boolean; + /** + * This getter is in sync with the "formValid" property of the partner-ui + */ + isFulfilled(): boolean; + /** + * Registers a callback and returns the function that unregisters it. Hosts that + * mount the same element again (dropin remounts its whole list on `setSession`) + * must call it on teardown, otherwise every mount leaves another live callback + * behind and a single `ready` fans out to all of them (SDK-12109). + * + * `remove()` deliberately keeps the map intact: `createPaymentElement` wires the + * splitter and submit plumbing through it once per instance, and the instance is + * cached and reused across remounts, so clearing it here would silently break + * split computation and payment submission. + */ + on(eventName: K, callback: PaymentElementEventsCallback[K]): () => void; + private attachUIListeners; + protected buildUI(): PursePartnerUI; + /** + * Validates the UI of the payment element. + * + * This method triggers the validation process of the UI component associated with the payment element. + * This is purely a form validation function. It can be useful if you want to trigger validation before submitting payment. + * + * @returns {Promise} A promise that resolves with true or the result of the validation + */ + validateUi(): Promise; + private clearListeners; +} declare class PurseHeadlessCheckoutError extends Error { readonly code: PurseHeadlessCheckoutErrorCodes; readonly documentationLink: string; @@ -2274,6 +2735,11 @@ declare const PurseHeadlessCheckoutErrors: { message: string; documentationLink: string; }; + readonly LOYALTY_ACCOUNT_NOT_FOUND: { + code: string; + message: string; + documentationLink: string; + }; readonly SECONDARY_TOKEN_EMPTY_BALANCE: { code: string; message: string; @@ -2342,6 +2808,7 @@ export declare interface PurseHeadlessCheckoutErrorsPayload { method: Method; uniqueness_token: string; }; + LOYALTY_ACCOUNT_NOT_FOUND?: never; SECONDARY_TOKEN_EMPTY_BALANCE?: never; USER_ACTION_REQUEST_HAS_EXPIRED: never; USER_ACTION_REQUEST_ABORTED_BY_CUSTOMER: never; @@ -2434,7 +2901,10 @@ export declare interface PurseHeadlessCheckoutEventPayload { partner: string; method: string; }; - HEADLESS_CHECKOUT_CLIENT_HOOK_ERROR: Record; + HEADLESS_CHECKOUT_CLIENT_HOOK_ERROR: { + error: unknown; + message?: string; + }; HEADLESS_CHECKOUT_CREATE: { sessionType: "v1" | "v2"; screen: { @@ -2528,32 +2998,22 @@ export declare interface PurseHeadlessCheckoutEventPayload { }; HEADLESS_CHECKOUT_VALIDATION_SUCCESS: { global_status: string; - results: { - method: string; - partner: string; - status: { - state: string; - code: string; - description?: string; - }; - }[]; + results: PurseHeadlessCheckoutValidationResultSummary[]; }; HEADLESS_CHECKOUT_VALIDATION_FAILED: { global_status: string; - results: { - method: string; - partner: string; - status: { - state: string; - code: string; - description?: string; - }; - }[]; + results: PurseHeadlessCheckoutValidationResultSummary[]; }; HEADLESS_CHECKOUT_PAYMENT_ERROR: { message?: string; api_status?: string; stack?: string; + /** Why the payment failed, attempt by attempt. Empty array when the API returned no result. */ + results?: PurseHeadlessCheckoutValidationResultSummary[]; + /** Whether the `/validate` response carried a redirection URL of its own. */ + has_redirection?: boolean; + /** Set when the failure came from a thrown error rather than from an API status. */ + error_name?: string; }; HEADLESS_CHECKOUT_PAYMENT_FULFILLED: { isFulfilled: boolean; @@ -2596,8 +3056,18 @@ export declare interface PurseHeadlessCheckoutEventPayload { message?: string; stack?: string; }; + /** + * A submission was refused by the re-entrancy guard: one was already in flight, or the session + * had already been validated. Counterpart of widget V3's + * `CHECKOUT_PRE_VALIDATION_ALREADY_ROLLING` / `CHECKOUT_CURRENT_SESSION_ALREADY_VALIDATED`. + */ + HEADLESS_CHECKOUT_SUBMIT_BLOCKED: { + state: PurseHeadlessCheckoutSessionState; + trigger: PurseHeadlessCheckoutSubmitTrigger; + }; HEADLESS_CHECKOUT_SUBMIT_PAYMENT: { splits: PaymentSplit[]; + trigger: PurseHeadlessCheckoutSubmitTrigger; }; HEADLESS_CHECKOUT_TOKEN_DELETE: { partner: string; @@ -2609,7 +3079,16 @@ export declare interface PurseHeadlessCheckoutEventPayload { method: string; tokenId?: string; }; - HEADLESS_CHECKOUT_UNCAUGHT_ERROR: Record; + HEADLESS_CHECKOUT_TOKEN_SET_FAVORITE: { + partner: string; + method: string; + tokenId?: string; + favorite: boolean; + }; + HEADLESS_CHECKOUT_UNCAUGHT_ERROR: { + stack: string; + message?: string; + }; HEADLESS_CHECKOUT_ON_BEFORE_VALIDATE_HOOK_FAILED: { message: string; error: { @@ -2690,6 +3169,25 @@ export declare interface PurseHeadlessCheckoutHostedFields extends Omit; + detectedBrands: Readable; + selectedBrand: Readable; + constructor(item: PurseHeadlessCheckoutPaymentItemBase, data: { + payment: Payment; + env: APIEnv; + registration?: Readable; + amount?: Readable; + interface?: Interface; + }, manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { + actualMethod?: Method; + sourceId?: string; + }) => Promise, onSessionTerminated?: () => void); + protected buildUI(): PursePartnerUI; + setSelectedBrand(brand: CardScheme): void; + setOptions(options?: Options): void; + render(): void; +} export declare interface PurseHeadlessCheckoutLoyaltyToken extends PurseHeadlessCheckoutTemporarySecondaryToken { method: "loyaltycard"; /** @@ -2844,15 +3342,25 @@ export declare interface PurseHeadlessCheckoutPaymentElement { * * @param {PaymentElementEventName} eventName - Name of the event to listen for * @param callback - Function to be called when the event occurs + * @returns A function that unregisters this callback. Call it when the host + * component unmounts: registering again on the same element without it leaves the + * previous callback live, and one event then fans out to every past registration. + * `remove()` does not unregister for you — the element instance is reused across + * remounts, so the callbacks it holds outlive it. * @example * ```typescript * // Listen for the UI ready event - * paymentElement.on('formValid', () => { + * const off = paymentElement.on('formValid', () => { * console.log('Payment UI form is valid'); * }); + * // …later, on teardown + * off(); * ``` */ - on(eventName: K, callback: PaymentElementEventsCallback[K]): void; + on(eventName: K, callback: PaymentElementEventsCallback[K]): () => void; +} +declare class PurseHeadlessCheckoutPaymentElement_2 extends PurseHeadlessCheckoutElement implements PurseHeadlessCheckoutPaymentElement { + appendTo(container: string | Element): void; } export declare type PurseHeadlessCheckoutPaymentItem = PurseHeadlessCheckoutPaymentMethod | PurseHeadlessCheckoutPaymentToken; /** @@ -3143,16 +3651,16 @@ export declare interface PurseHeadlessCheckoutPrimaryToken extends PurseHeadless /** Whether this token is the user's favorite payment method. Reflects `IntegratedToken.favorite`. */ isFavorite: Readable; /** - * Mark this token as the user's favorite. Persists the change via the wallet API. + * Toggle this token's favorite status. If it is already a favorite, it will be removed; + * otherwise it will be marked as the user's favorite. Persists the change via the wallet API. * * @throws - {@link PurseHeadlessCheckoutError} `MISSING_WALLET_SESSION` \ * If no wallet session is loaded - * @throws - {@link PurseHeadlessCheckoutError} `MISSING_TOKEN` \ - * If the token is not found in the wallet * * @example * ```typescript - * await token.setAsFavorite(); + * await token.setAsFavorite(); // marks as favorite + * await token.setAsFavorite(); // removes favorite status * ``` */ setAsFavorite(): Promise; @@ -3176,10 +3684,20 @@ export declare interface PurseHeadlessCheckoutRegisterable { name?: string; }): Promise; /** - * This property exposes the saveToken configuration of the method. - * A mean of payment cannot be saved if the method is not registerable. + * Whether the user can choose to register this method. + * True only for `save_token_mode` values `PREFER` and `PREFER_NO`. + * When false, the save-token checkbox must not be shown; the backend + * enforces the registration policy (ALWAYS/NEVER) without a frontend signal. */ canBeRegistered: boolean; + /** + * The effective `save_token_mode` resolved for this mean of payment + * (`ALWAYS` / `PREFER` / `PREFER_NO` / `NEVER`), including the `save_token` + * boolean fallback. Exposed for consumers that need to distinguish *why* + * registration is or isn't user-controllable — e.g. `ALWAYS` forces + * registration silently, with `canBeRegistered` false like `NEVER`. + */ + saveTokenMode: SaveTokenMode; /** * This property exposes if the mean of payment is bound for registration upon validate. */ @@ -3195,6 +3713,15 @@ export declare type PurseHeadlessCheckoutSecondaryMethod = PurseHeadlessCheckout limit?: number; /** Always true for secondary payment methods */ isSecondary: true; + /** + * Secondary methods (gift cards, vouchers, loyalty) have no payment element — they are + * entered via `getSecondaryToken()` / `take()`, not a rendered form. Calling this warns + * (with a documentation link) and returns `undefined` so the integration degrades + * gracefully instead of throwing a `TypeError`. + * + * @see https://docs.purse.tech/docs/integrate/purse-checkout/headless-checkout/advanced-guides/render-payment-methods#secondary-methods + */ + getPaymentElement(): undefined; } & SecondarySpecificConfig; /** * Represents a secondary token in the Purse checkout system. @@ -3212,6 +3739,15 @@ export declare type PurseHeadlessCheckoutSecondaryMethod = PurseHeadlessCheckout export declare interface PurseHeadlessCheckoutSecondaryToken extends PurseHeadlessCheckoutToken { type: "token" | "temporary_token"; isSecondary: true; + /** + * Secondary tokens (gift cards, vouchers, loyalty) have no payment element — they are + * applied via `take()`, not a rendered form. Calling this warns (with a documentation + * link) and returns `undefined` so the integration degrades gracefully instead of + * throwing a `TypeError`. + * + * @see https://docs.purse.tech/docs/integrate/purse-checkout/headless-checkout/advanced-guides/render-payment-methods#secondary-methods + */ + getPaymentElement(): undefined; limit: number; expiration_date: string | Date; /** @@ -3301,6 +3837,8 @@ export declare interface PurseHeadlessCheckoutSecondaryTokenProvider { * If secondary token creation fails * - {@link PurseHeadlessCheckoutError} `FAILED_TO_INIT_SECONDARY_TOKEN` \ * If secondary token initialization fails + * - {@link PurseHeadlessCheckoutError} `LOYALTY_ACCOUNT_NOT_FOUND` \ + * If the customer has no loyalty account on the partner side * - {@link PurseHeadlessCheckoutError} `MISSING_SESSION` \ * If the session is missing * - {@link PurseHeadlessCheckoutError} `PAYMENT_METHOD_NOT_FOUND` \ @@ -3315,6 +3853,14 @@ export declare interface PurseHeadlessCheckoutSecondaryTokenProvider { getSecondaryToken(pan: string, cvv?: string, signal?: AbortSignal): Promise; } export declare type PurseHeadlessCheckoutSessionState = "idle" | "ready" | "submitting" | "submitted" | "error" | "expired" | "terminated"; +/** + * How a payment submission was triggered. + * + * - `api`: the merchant called `submitPayment()`. + * - `xpay`: an express/wallet plugin (Apple Pay, Google Pay…) requested validation + * itself, without the merchant calling `submitPayment()`. + */ +export declare type PurseHeadlessCheckoutSubmitTrigger = "api" | "xpay"; export declare interface PurseHeadlessCheckoutTemporarySecondaryToken extends PurseHeadlessCheckoutSecondaryToken, PurseHeadlessCheckoutRegisterable { type: "temporary_token"; } @@ -3372,6 +3918,22 @@ export declare type PurseHeadlessCheckoutV1SessionData = LegacyPaymentSession; * @group Interfaces */ export declare type PurseHeadlessCheckoutV2Params = string; +/** + * Per-attempt detail extracted from a `/validate` response. Kept flat and small: it travels with + * every validation event, including the error ones, so that a failed payment can be diagnosed from + * the log alone without correlating several events. + */ +export declare interface PurseHeadlessCheckoutValidationResultSummary { + method?: string; + partner?: string; + status: { + state?: string; + code?: string; + description?: string; + }; + /** `plugin_result.status`, the partner-side outcome behind the Purse status code. */ + plugin_status?: string; +} declare class PursePayment implements Payment { readonly factoriesStaticData: { manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { @@ -3392,6 +3954,18 @@ declare class PursePayment implements Payment { methods: Readable; tokens: Readable; walletTokens: Readable; + /** + * Primary-method element cache, keyed by the couple's stable `partner-method` + * id. `setSession` rebuilds `mops` with brand-new item objects on every + * refresh; this registry lets `buildPrimaryPaymentMethod` hand back the same + * `` element (and its `registration` store) for a couple + * whose `configurations` are unchanged, instead of tearing it down — see + * `hasSameMethodConfiguration` usage in `setSession` below. + */ + readonly primaryMethodElementRegistry: Map; + registration: Writable; + }>; constructor(env: APIEnv, factoriesStaticData: { manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { actualMethod?: Method; @@ -3431,6 +4005,7 @@ export declare type RegistrationState = { registered: boolean; name: string | null; }; +export declare type SaveTokenMode = "ALWAYS" | "PREFER" | "PREFER_NO" | "NEVER"; /** * Secondaries are true Headless payment methods and are handled directly by the SDK for now. * They are not handled by the partner UI. @@ -3650,6 +4225,11 @@ declare const DropinThemeSchema: z.ZodObject<{ } | undefined; }>; export type DropinTheme = z.infer; +export type SaveTokenEventPayload = { + partner: string; + method: string; + save_token_mode: SaveTokenMode; +}; export interface DropinPrivateEventPayload { DROPIN_CHECKOUT_INIT_OPTIONS: { hidePayButton: boolean; @@ -3714,18 +4294,9 @@ export interface DropinPrivateEventPayload { redirect_domain?: string; redirect_method?: string; }; - DROPIN_CHECKOUT_SAVE_TOKEN_ENABLED: { - partner: string; - method: string; - }; - DROPIN_CHECKOUT_SAVE_TOKEN_OPT_IN: { - partner: string; - method: string; - }; - DROPIN_CHECKOUT_SAVE_TOKEN_OPT_OUT: { - partner: string; - method: string; - }; + DROPIN_CHECKOUT_SAVE_TOKEN_ENABLED: SaveTokenEventPayload; + DROPIN_CHECKOUT_SAVE_TOKEN_OPT_IN: SaveTokenEventPayload; + DROPIN_CHECKOUT_SAVE_TOKEN_OPT_OUT: SaveTokenEventPayload; DROPIN_CHECKOUT_SET_SESSION_CALLED: { session_id: string; }; @@ -3875,6 +4446,22 @@ export interface DropinPrivateEventPayload { sessionId: string; errors: ZodIssue[]; }; + DROPIN_CHECKOUT_CONFIG_UPDATED: { + hasTheme: boolean; + locale: string | undefined; + hidePayButton: boolean; + }; + DROPIN_CHECKOUT_REMOTE_CONFIG_LOADED: { + entityId: string; + }; + DROPIN_CHECKOUT_REMOTE_CONFIG_NOT_FOUND: { + entityId: string; + status: number; + }; + DROPIN_CHECKOUT_REMOTE_CONFIG_FAILED: { + entityId: string; + reason: string; + }; DROPIN_CHECKOUT_LOYALTY_DISPLAYED: { totalEuros: number; eligibleEuros: number; @@ -3905,6 +4492,22 @@ export interface DropinEventBusEvent[0]; +/** + * Per-method highlight configuration, exposed through the dropin `highlightMethods` + * init/config option. A merchant can flag one or more payment methods with a custom + * tag (label + optional background color). + * + * - `partner` is an optional filter: when omitted, the highlight applies to every + * partner exposing that `method`. + * - `color` is any valid CSS color. When omitted (or invalid), the tag falls back + * to the default theme styling. + */ +export type DropinHighlightMethod = { + method: string; + partner?: string; + label: string; + color?: string; +}; declare const translationFiles: { readonly "de-AT": { translation: { @@ -3978,7 +4581,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -3986,24 +4588,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -4189,7 +4787,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -4197,24 +4794,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -4400,7 +4993,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -4408,24 +5000,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -4632,7 +5220,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -4640,24 +5227,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -4864,7 +5447,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -4872,24 +5454,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -5096,7 +5674,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -5104,24 +5681,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -5328,7 +5901,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -5336,24 +5908,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -5560,7 +6128,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -5568,24 +6135,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -5792,7 +6355,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -5800,24 +6362,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -6024,7 +6582,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -6032,24 +6589,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -6256,7 +6809,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -6264,24 +6816,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -6488,7 +7036,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -6496,24 +7043,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -6720,7 +7263,6 @@ declare const translationFiles: { panInputLabel: string; panRequiredError: string; panFormatError: string; - panCannotBeEmptyError: string; panTooltipText: string; cvvPlaceholder: string; cvv4Placeholder: string; @@ -6728,24 +7270,20 @@ declare const translationFiles: { cvv4InputLabel: string; cvvRequiredError: string; cvvFormatError: string; - cvvCannotBeEmptyError: string; cvvTooltipText: string; expirationPlaceholder: string; expirationInputLabel: string; expirationRequiredError: string; expirationFormatError: string; - expirationCannotBeEmptyError: string; expirationOutOfRangeError: string; expirationTooltipText: string; holderPlaceholder: string; holderInputLabel: string; holderRequiredError: string; holderFormatError: string; - holderCannotBeEmptyError: string; birthdateInputLabel: string; birthdateRequiredError: string; birthdateFormatError: string; - birthdateCannotBeEmptyError: string; saveTokenLabel: string; saveAccountLabel: string; }; @@ -6887,15 +7425,21 @@ export type InitOptions = { theme?: DropinTheme; locale?: keyof typeof translationFiles; hidePayButton?: boolean; + highlightMethods?: DropinHighlightMethod[]; hooks?: { onRefreshSession?: () => PurseHeadlessCheckoutV1Params["paymentSession"] | string | void | Promise; }; }; +export type DropinConfig = { + theme: DropinTheme; + locale?: keyof typeof translationFiles; + hidePayButton: boolean; + highlightMethods?: DropinHighlightMethod[]; +}; declare class PurseDropin { private readonly _headlessCheckoutInstance; - private readonly _theme; - private readonly _locale?; - private readonly _hidePayButton; + private _config; + private readonly _remoteTheme; private readonly _onRefreshSession?; private _app?; private readonly _eventBus; @@ -6909,11 +7453,12 @@ declare class PurseDropin { }[]>; sessionState: Readable; private constructor(); - static init({ session, eventListener, theme, locale, hidePayButton, hooks }: InitOptions): Promise; + static init({ session, eventListener, theme, locale, hidePayButton, highlightMethods, hooks, }: InitOptions): Promise; setSession(session: PurseHeadlessCheckoutV1Params["paymentSession"] | string): Promise; setWalletSession(walletSession: OnePayWalletSession$2): Promise; submitPayment(): Promise; mount(target: HTMLElement): Promise; + setConfig(config: Partial): void; unmount(): Promise; } export declare function createDropinCheckout(options: InitOptions): Promise; diff --git a/src/generated/types/headless-checkout.d.ts b/src/generated/types/headless-checkout.d.ts index 643efb5..0fd4af2 100644 --- a/src/generated/types/headless-checkout.d.ts +++ b/src/generated/types/headless-checkout.d.ts @@ -3,6 +3,8 @@ /// import { AnyObject } from 'final-form'; +import { CSSResult, LitElement, PropertyValues, TemplateResult as TemplateResult_2, nothing } from 'lit'; +import { TemplateResult } from 'lit-html'; export type Subscriber = (value: T) => void; export type Unsubscriber = () => void; @@ -140,6 +142,7 @@ export declare const Partners: { readonly illicado: "illicado"; readonly ingenico: "ingenico"; readonly klarna: "klarna"; + readonly lyra: "lyra"; readonly mercanet: "mercanet"; readonly oney: "oney"; readonly oneycard: "oneycard"; @@ -152,6 +155,7 @@ export declare const Partners: { readonly riverty: "riverty"; readonly scalapay: "scalapay"; readonly scellius: "scellius"; + readonly sequra: "sequra"; readonly sipsv2: "sipsv2"; readonly slimpay: "slimpay"; readonly sogecommerce: "sogecommerce"; @@ -256,6 +260,14 @@ export type IntegratedToken = { source?: "MERCHANT" | "SHARED_WALLET"; savable_to_shared_wallet?: boolean; created_at?: string; + /** + * Network-token counterpart of this card token. When `available` is true and + * the experience enables network tokens (`settings.network_token`), the + * payment goes through the network token and no CVV is collected. + */ + network_token?: { + available?: boolean; + }; }; export interface MethodConfiguration { stable: boolean; @@ -271,12 +283,25 @@ export interface MethodConfiguration { settings: { registration_mode?: string; save_token?: boolean; + save_token_mode?: "ALWAYS" | "PREFER" | "PREFER_NO" | "NEVER"; direct_debit?: boolean; + /** + * Front-settings gate for network tokens. Tokens carrying an available + * `network_token` only pay through it (and hide their CVV) when this is + * true. + */ + network_token?: boolean; payment_min_amount?: number; payment_max_amount?: number; usage_per_session_limit?: number; merchant_specifics?: { integration_mode?: "redirection" | "popup"; + /** + * When truthy, the CVV field is hidden and not required when paying with + * a saved token. Configured per experience in the merchant back-office, + * so the value can reach the front as a string. + */ + pay_token_no_cvv?: boolean | "true" | "false"; }; }; } @@ -455,6 +480,11 @@ declare const PurseErrors: { message: string; documentationLink: string; }; + readonly API_UNKNOWN_ERROR: { + code: string; + message: string; + documentationLink: string; + }; }; export type PurseErrorCodes = keyof typeof PurseErrors; export type JsonSerializable = string | number | boolean | null | { @@ -554,7 +584,10 @@ export type ItemValidationData> = { partner: string; session_id: string; transaction_id: string; - plugin_result: Result; + plugin_result: Result & { + status?: string; + amount?: number; + }; status: { action: TransactionAction; state: StatusState; @@ -619,19 +652,33 @@ export interface LoanSimulation { export type WalletItem = { id: string; name?: string; + favorite?: boolean; }; declare class ApiError extends PurseError { readonly path: string; readonly error: string; responseHeaders?: Record; + /** HTTP status of the response that produced this error, when available (e.g. 400, 429, 500). */ + status?: number; constructor(error: (typeof PurseErrors)[PurseErrorCodes], path?: string, errorCode?: string, message?: string); } +/** + * Which product an SDK is embedded in, when it can be embedded by another. + * + * `direct` means the merchant integrated it themselves. Always sent explicitly so a + * missing value means "SDK build predates wrapper reporting" rather than "direct". + * + * @internal Not part of the public integration surface — set by the wrapping product. + */ +export type MonitoringWrapper = "direct" | "dropin"; export interface MonitoringApiEventMessage { instanceId?: string; entityId?: string; paymentSessionId?: string; walletSessionId?: string; version?: string; + /** @internal See {@link MonitoringWrapper}. */ + wrapper?: MonitoringWrapper; env?: EnvironmentTarget; event: Event; } @@ -661,11 +708,12 @@ export interface WalletAPI { deleteAllTokens(): Promise; setAsFavorite(data: { id: string; + favorite: boolean; }): Promise; } export interface MonitoringAPI { getApiKey(): string; - patchExtrasData(E: Pick, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version">): void; + patchExtrasData(E: Pick, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version" | "wrapper">): void; sendLogs(E: E[]): void; } export type APIPaths = { @@ -681,7 +729,7 @@ declare class FetchMonitoringAPI implements MonitoringAPI { private extras; constructor(env: EnvironmentTarget, root: string, apiKey: string); getApiKey(): string; - patchExtrasData: (payload: Partial, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version">>) => void; + patchExtrasData: (payload: Partial, "instanceId" | "entityId" | "paymentSessionId" | "walletSessionId" | "version" | "wrapper">>) => void; sendLogs: (events: Event[]) => void; } export declare type ButtonOptions = google.payments.api.ButtonOptions; @@ -694,6 +742,42 @@ export declare interface CreditCardValidateUiResult { export declare type DeepPartial = T extends any[] ? T : { [P in keyof T]?: DeepPartial; }; +export declare type EnvSource = { + id: string; + singleton?: boolean; + production: Source; +} & { + [key in ExternalSourceEnv]?: Source; +}; +export declare type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]; +declare const ErrorCodes: { + readonly MethodIsNotImplementedByPlugin: "MethodIsNotImplementedByPlugin"; + readonly PluginEncounteredALethalException: "PluginEncounteredALethalException"; + readonly PluginIsIncompatibleWithClient: "PluginIsIncompatibleWithClient"; + readonly PreValidationIsntAvailableForPayButton: "PreValidationIsntAvailableForPayButton"; + readonly PostValidationDataIsMissingInformations: "PostValidationDataIsMissingInformations"; + readonly CannotInitWithoutSession: "CannotInitWithoutSession"; + readonly CannotSubmitWithoutClient: "CannotSubmitWithoutClient"; + readonly CannotPreValidateWithoutPlugin: "CannotPreValidateWithoutPlugin"; + readonly FormValidationFailed: "FormValidationFailed"; + readonly ClientNotFoundWhenSettingSession: "ClientNotFoundWhenSettingSession"; + readonly ClientNotFoundAfterSourceLoad: "ClientNotFoundAfterSourceLoad"; + readonly PreValidationFailed: "PreValidationFailed"; + readonly PostValidationFailed: "PostValidationFailed"; + readonly DynamicAmountNotInSessionRange: "DynamicAmountNotInSessionRange"; + readonly CantSetBrandThatIsNotAmongMatches: "CantSetBrandThatIsNotAmongMatches"; + readonly CannotPostValidateWithoutPlugin: "CannotPostValidateWithoutPlugin"; + readonly MissingDataFromContext: "MissingDataFromContext"; + readonly "3DSCantProceedWithoutRedirectionURL": "3DSCantProceedWithoutRedirectionURL"; + readonly "3DSFingerprintingFailed": "3DSFingerprintingFailed"; + readonly APIResultUnsatisfying: "APIResultUnsatisfying"; + readonly PluginInitFailed: "PluginInitFailed"; + readonly TargetNotInDocument: "TargetNotInDocument"; + readonly MissingTargetFromConfig: "MissingTargetFromConfig"; + readonly MultipleMatchesForTargetInDocument: "MultipleMatchesForTargetInDocument"; + readonly PartnerDoesNotSupportUI: "PartnerDoesNotSupportUI"; + readonly HolderNameFieldRequired: "HolderNameFieldRequired"; +}; export declare type ErrorPayload = { reason?: string; } & Record; @@ -889,6 +973,25 @@ declare const Events: { "partnerError" ]; }; + /** + * The plugin's own integration broke for this attempt — a dead popup, a + * severed window transport — and the failure will not clear by retrying, but + * it is not permanent either: a fresh amount or session can make the method + * work again. + * + * Distinct from `partnerError`, which also carries ordinary payment outcomes + * (Lyra reports declined cards through it). Sharing both topics means every + * existing `error`/`partnerError` subscriber keeps receiving it; what the host + * does about it — deactivating the method, and when to lift that — is the + * host's policy, not the plugin's. + */ + readonly partnerRecoverableError: { + readonly code: "partnerRecoverableError"; + readonly topics: readonly [ + "error", + "partnerError" + ]; + }; readonly change: { readonly code: "change"; readonly topics: readonly [ @@ -956,9 +1059,10 @@ declare const Events: { ]; }; }; +export declare type ExternalSourceEnv = "production" | "sandbox" | "test" | "development"; export declare type FieldValidationError = (typeof FieldValidationErrors)[keyof typeof FieldValidationErrors]; declare const FieldValidationErrors: { - readonly cannotBeEmpty: "cannotBeEmpty"; + readonly required: "required"; readonly dateOutOfRange: "dateOutOfRange"; readonly format: "format"; readonly lengthOutOfRange: "lengthOutOfRange"; @@ -1064,19 +1168,24 @@ export declare interface HostedFieldsUIOptions { * Some partners displays a complete form and let you configure the text through a JS configuration. We've mapped those * texts and let you override all off them form this single object. During a session, while displaying a form some of * the variables might not be used, it depends on the context. + * + * Every field is plain text and is rendered as such. The only exceptions are `consentText` and + * `loanSimulation`, which accept limited HTML — see their own docs. */ declare interface HostedFormUIOptions { panPlaceholder?: string; panInputLabel?: string; panRequiredError?: string; - panFormatError?: string; + /** @deprecated use panRequiredError */ panCannotBeEmptyError?: string; + panFormatError?: string; panTooltipText?: string; birthdatePlaceholder?: string; birthdateInputLabel?: string; birthdateRequiredError?: string; - birthdateFormatError?: string; + /** @deprecated use birthdateRequiredError */ birthdateCannotBeEmptyError?: string; + birthdateFormatError?: string; birthdateOutOfRangeError?: string; birthdateTooltipText?: string; cvvPlaceholder?: string; @@ -1087,6 +1196,9 @@ declare interface HostedFormUIOptions { * This value is interpolated with {url:string} * use {{variable}} to use interpolation * @example 'Your will agree to the tos ' + * + * Accepts HTML, sanitised by allowlist: a, b, strong, em, i, u, span, br, p, ul, ol, li, + * with href/target/rel/style only. `href` must be an absolute https URL. */ consentText?: string; /** @@ -1098,8 +1210,9 @@ declare interface HostedFormUIOptions { */ cvv4InputLabel?: string; cvvRequiredError?: string; - cvvFormatError?: string; + /** @deprecated use cvvRequiredError */ cvvCannotBeEmptyError?: string; + cvvFormatError?: string; cvvTooltipText?: string; /** * When true, the CVV field is hidden and not required for payment. @@ -1114,30 +1227,35 @@ declare interface HostedFormUIOptions { expirationPlaceholder?: string; expirationInputLabel?: string; expirationRequiredError?: string; - expirationFormatError?: string; + /** @deprecated use expirationRequiredError */ expirationCannotBeEmptyError?: string; + expirationFormatError?: string; expirationOutOfRangeError?: string; expirationTooltipText?: string; holderPlaceholder?: string; holderInputLabel?: string; holderRequiredError?: string; - holderFormatError?: string; + /** @deprecated use holderRequiredError */ holderCannotBeEmptyError?: string; + holderFormatError?: string; ibanPlaceholder?: string; ibanInputLabel?: string; ibanRequiredError?: string; - ibanFormatError?: string; + /** @deprecated use ibanRequiredError */ ibanCannotBeEmptyError?: string; + ibanFormatError?: string; emailPlaceholder?: string; emailInputLabel?: string; emailRequiredError?: string; - emailFormatError?: string; + /** @deprecated use emailRequiredError */ emailCannotBeEmptyError?: string; + emailFormatError?: string; phonePlaceholder?: string; phoneInputLabel?: string; phoneRequiredError?: string; - phoneFormatError?: string; + /** @deprecated use phoneRequiredError */ phoneCannotBeEmptyError?: string; + phoneFormatError?: string; /** * This value is interpolated with: * - first @@ -1147,6 +1265,8 @@ declare interface HostedFormUIOptions { * - cost * - taeg * use {{variable}} to use interpolation + * + * Accepts the same HTML allowlist as `consentText`. */ loanSimulation?: string; /** @@ -1166,16 +1286,13 @@ declare const IntegrationTypes: { readonly xPayButton: "xPayButton"; readonly redirection: "redirection"; }; -export declare interface KRError_2 { - errorCode: string; - errorMessage: string; - detailedErrorCode?: string; - detailedErrorMessage?: string; - field?: string; - children?: Array; - metadata?: Record; -} -export declare type ManagedHostedFieldValue = "valid" | Extract | null; +export declare type Interface = (typeof Interfaces)[keyof typeof Interfaces]; +declare const Interfaces: { + readonly form: "form"; + readonly hostedfields: "hostedfields"; + readonly payButton: "payButton"; +}; +export declare type ManagedHostedFieldValue = "valid" | Extract | null; /** * Options let you override theme configuration and translations on our UIs. */ @@ -1211,6 +1328,173 @@ declare type Options = { */ locale?: string; }; +/** + * SDK-agnostic partner error contract. `events.ts` must not know any specific + * partner SDK's internals — the normalized `message` is the stable contract + * consumers read; the raw partner error is carried opaquely in `originalError`. + */ +export declare interface PartnerErrorPayload { + /** Normalized, contextual message. The stable contract consumers read. */ + message: string; + /** Raw partner SDK error. Opaque — debug only, never introspect its shape or cast it. */ + originalError?: unknown; +} +declare class PartnerUI> extends LitElement { + #private; + private sourceDownloaded; + private plugin; + private extraSources; + private data; + private readonly context; + session: LegacyPaymentSession | undefined; + env: APIEnv | null; + options: Options; + amount?: number; + register: boolean | undefined; + partner: Partner; + method: Method; + interface: Interface; + token?: Token; + /** + * A payzen xpay specific function to trigger onBeforeValidate hook outside the payment process + * This function should throw if the onBeforeValidate fails, allowing us to `return false` in the `onClick` of the smartForm + * Returning false in the onClick prevents to submit + */ + __xpay_payzen_specific_triggerOnBeforeValidate__: (method: Extract) => Promise; + constructor(); + private get envName(); + dispatchUnifiedEvent: (code: K, payload?: UnifiedEventPayload[K] | null) => void; + preValidation(): Promise; + postValidation(data: ValidationResultData, redirectionHandler?: RedirectionHandler): Promise; + /** + * Use this method to run the validation of the UI. It can be useful to trigger a validation of the UI, in a "onsubmit" validation workflow for instance. + * This should not be confused with the "validate" concept which is the API call to validate the payment. + * */ + validateUi(): Promise; + /** + * When detected brands are multiple, use this method to set the selected brand. + * default is detectedBrands[0] + * @param brand the brand to select + * @returns void + **/ + setSelectedBrand(brand: CardScheme): void; + connectedCallback(): void; + /** + * The element has no shadow root and is mounted into merchant-controlled DOM, so anything that + * serialises markup — session replay, error reporters, extension content scripts — captures + * attribute values. `session` alone carries every saved card in the session. These three stay + * supported as attribute *inputs* (`attribute: false` would silently starve callers that still + * pass them, breaking saved-card payments with no error), but nothing is left behind in the DOM. + * SDK-12163. + * + * Called from `connectedCallback`, which is where it is safe: custom-element upgrade replays + * `attributeChangedCallback` for existing attributes *before* `connectedCallback`, so Lit has + * already parsed them, and `connectedCallback` runs synchronously inside `appendChild`, so no + * script can read them in between. + * + * The value has to be re-assigned after removal: `removeAttribute` re-enters Lit's + * `attributeChangedCallback` with `null`, and the default `type: Object` converter maps that + * back to `null` — removing the attribute would otherwise wipe the property it just parsed. + * + * Only ever scrub inputs that callers pass by *property*. Svelte caches the attribute values it + * wrote on the node and skips a same-value rewrite, so an attribute removed behind its back + * never comes back — `amount`, `partner`, `method` and `interface` must stay out of here. + */ + private scrubSecretAttributes; + disconnectedCallback(): void; + performUpdate(): void; + private updateData; + private loadExtraSources; + private getSourceListener; + updated(changedProperties: PropertyValues): void; + willUpdate(changedProperties: PropertyValues): void; + createRenderRoot(): this; + render(): typeof nothing | TemplateResult<1>; +} +/** + * The partner ui context is the piece of data & abilities that we give to the plugins to operate with. + */ +export declare interface PartnerUIContext> { + /** + * The context provides a way to dispatch UnifiedEvent. This is required because our plugins is responsible for + * dispatching key events like fulfillment. + */ + dispatchEvent: (code: K, payload?: UnifiedEventPayload[K] | null) => void; + /** + * This is a reference to the partner ui root node, it is useful for plugins that need to append elements to the DOM + */ + shadowRoot: HTMLElement; + /** + * Data in the context is the "reactive" part, so you can read it through an accessor. + */ + data: () => Data; + /** + * This is the setter for the data. It takes a partial object of Data and patch the current state with it. + * If the references you passed changed the original ones, it will rerender the UI and also call the plugin's onDataChanged + */ + updateData: (data: Partial) => void; + /** + * The current payment session, can be undefined if no session is set. + */ + session?: LegacyPaymentSession; + /** + * This amount could be less than the session total, and plugins might want to perform action when it changes. + * This amount is less than the total when a secondary is involved for instance. + */ + amount?: number; + /** + * The redirection interface allows the plugin to gracefully redirect to another location. + */ + redirection: RedirectionHandler; + /** + * A helper to make calls to the API. We give it the API Root. + */ + api: PaymentAPI; + /** + * The env we're running the plugin in. This is useful for plugins that need to download extra sources. + */ + envName?: EnvironmentTarget; + /** + * Gives the ability to download extra sources at runtime + */ + loadExtraSources: (sources: EnvSource[]) => Promise; + /** + * If the partner UI renders a token, this will be set with the token extracted from session + */ + token?: Token; + /** + * The current partner ui requested interface + */ + ui: Interface; + /** + * The current partner ui's options + */ + options: Options; + /** + * Partner ui theme + */ + theme: Theme; + /** + * If the partners create some sort of client (Securefields, paypal...), its reference can be stored here + */ + client?: Client; + /** + * Current partner ui register attribute. + */ + register?: boolean; + /** + * This is a wild card that can be updated by the plugin to keep references without re-rendering the view + */ + dmz: { + initialStepId: number | null; + } & Record; + /** + * A payzen xpay specific function to trigger onBeforeValidate hook outside the payment process + * This function should throw if the onBeforeValidate fails, allowing us to `return false` in the `onClick` of the smartForm + * Returning false in the onClick prevents to submit + */ + __xpay_payzen_specific_triggerOnBeforeValidate__: (method: Extract) => Promise; +} /** * A theme is carrying all the style variables use across our UIs. * global can be seen as a default values store on which the other scopes will rely on to fall back a value. @@ -1325,6 +1609,13 @@ declare type PartnerUITheme = { helperText: ThemeScope<"helperText">; tooltip: ThemeScope<"tooltip">; }; +declare class PursePartnerUI> extends PartnerUI { +} +export declare interface RedirectionHandler { + submitRedirectionForm(action: string, serializedFormData: string): Promise | void; + submitStringRedirectionForm(id: string, responseFormAsString: string): Promise | void; + redirectToUrl(url: string): Promise | void; +} export declare interface RedirectionUIOptions { title?: string; } @@ -1332,6 +1623,37 @@ export declare type Source = { type: "script" | "link"; attributes: Record; }; +declare class Theme { + static readonly UNEXPOSED: { + "background-color": string; + "brand-select-width": string; + "gap-small": string; + "gap-tiny": string; + "input-height": string; + "divider-margin": string; + "divider-bg": string; + }; + static readonly defaultTheme: PartnerUIOptionsTheme; + private _currentTheme; + private readonly _setProperty; + constructor(_setProperty: Theme["_setProperty"], theme?: PartnerUIOptionsTheme); + setTheme(theme?: PartnerUIOptionsTheme): void; + getValue(scope: Scope, key: keyof PartnerUITheme[Scope], variant?: ThemeScopesVariants): string | undefined; + getGlobalOverrides(): CSSResult; + applyCurrentTheme(): void; + private get currentTheme(); + private set currentTheme(value); + /** + * This method is in charge of creating a complete theme (not partial) using the default theme. + * if the `input['::placeholder']['color']` is not defined + * It will fall back to `input['color']` + * then to `global['::placeholder']['color']` + * and then to `global['color']` + * @param partialThemeFromOptions + * @private + */ + private scaffoldThemeValues; +} declare const THEME_SCOPES: readonly [ "global", "input", @@ -1433,6 +1755,16 @@ export declare type ThemeScopesVariants = ThemeSco export declare type ThemeScopesVariantsMap = { [K in ThemeScopeName]: (typeof THEME_SCOPES_VARIANTS)[K][number]; }; +export declare type Token = { + id: string; + expiration_date: string; + value: string; + selected_network: string; + holder_name: string; + description: { + display_token: string; + }; +}; export declare interface UnifiedEventPayload { clientError: ErrorPayload | null; clientNotFound: ErrorPayload | null; @@ -1485,9 +1817,13 @@ export declare interface UnifiedEventPayload { interface: string; fallBackInterface: string; }; + /** `code`/`reason` carry the rejecting `UnifiedError`, so the log says which check refused. */ pluginIsIncompatibleWithClient: { partner: Partner; method: Method; + code?: ErrorCode; + reason?: string; + stack?: string; }; pluginPostValidation: Record | null; pluginPreValidation: Record | null; @@ -1507,8 +1843,14 @@ export declare interface UnifiedEventPayload { } | null; other: Record | null; partnerError: { - error: Omit; + error: PartnerErrorPayload; method: string; + partner: string; + }; + partnerRecoverableError: { + error: PartnerErrorPayload; + method: string; + partner: string; }; change: HostedFieldsEventPayload | null; focus: HostedFieldsEventPayload | null; @@ -1543,16 +1885,45 @@ declare interface XPayButtonUIOptions { * @example 'fr-FR' */ locale?: string; + /** + * Text preceding the Apple Pay logo, on partners rendering their own button label (e.g. Lyra smart form). + * Native Apple Pay buttons ignore it, they carry their own localized wording. + * Defaults to a locale-based translation. + * @example 'Buy with' + */ + label?: string; }; google?: GooglePayButtonOptions & { height?: number; + /** + * Text preceding the Google Pay logo, on partners rendering their own button label (e.g. Lyra smart form). + * Native Google Pay buttons ignore it, they carry their own localized wording. + * Defaults to a locale-based translation. + * @example 'Buy with' + */ + label?: string; }; paypal?: { + /** + * @deprecated Ignored since the migration to PayPal JS SDK v6: layout was a + * property of the v5 multi-button `paypal.Buttons()` container, and v6 renders + * exactly one button web component per `` element. Kept for + * source compatibility — remove on the next breaking release. + */ layout?: "vertical" | "horizontal"; color?: "silver" | "white" | "black" | "gold" | "blue"; - shape?: "rect" | "pill"; + shape?: "rect" | "pill" | "sharp"; height?: number; - label?: "paypal" | "checkout" | "buynow" | "pay" | "installment"; + /** + * Button wording. `paypal` renders the wordmark alone and is the default; + * `installment` was dropped by the PayPal JS SDK v6 and falls back to it. + */ + label?: "paypal" | "checkout" | "buynow" | "pay" | "subscribe" | "donate" | "installment"; + /** + * Corner radius of the button, in pixels. Takes precedence over `shape`. + * See https://developer.paypal.com/sdk/js/reference/#borderRadius + */ + borderRadius?: number; /** * When true, removes the max-width constraint applied by PayPal on the button container. * See https://developer.paypal.com/sdk/js/reference/#width @@ -1688,15 +2059,22 @@ export interface PurseHeadlessCheckoutPaymentElement { * * @param {PaymentElementEventName} eventName - Name of the event to listen for * @param callback - Function to be called when the event occurs + * @returns A function that unregisters this callback. Call it when the host + * component unmounts: registering again on the same element without it leaves the + * previous callback live, and one event then fans out to every past registration. + * `remove()` does not unregister for you — the element instance is reused across + * remounts, so the callbacks it holds outlive it. * @example * ```typescript * // Listen for the UI ready event - * paymentElement.on('formValid', () => { + * const off = paymentElement.on('formValid', () => { * console.log('Payment UI form is valid'); * }); + * // …later, on teardown + * off(); * ``` */ - on(eventName: K, callback: PaymentElementEventsCallback[K]): void; + on(eventName: K, callback: PaymentElementEventsCallback[K]): () => void; } export interface PurseHeadlessCheckoutHostedFields extends Omit { supportedBrands: Readable; @@ -1737,6 +2115,8 @@ export interface PurseHeadlessCheckoutSecondaryTokenProvider { * If secondary token creation fails * - {@link PurseHeadlessCheckoutError} `FAILED_TO_INIT_SECONDARY_TOKEN` \ * If secondary token initialization fails + * - {@link PurseHeadlessCheckoutError} `LOYALTY_ACCOUNT_NOT_FOUND` \ + * If the customer has no loyalty account on the partner side * - {@link PurseHeadlessCheckoutError} `MISSING_SESSION` \ * If the session is missing * - {@link PurseHeadlessCheckoutError} `PAYMENT_METHOD_NOT_FOUND` \ @@ -1985,6 +2365,11 @@ declare const PurseHeadlessCheckoutErrors: { message: string; documentationLink: string; }; + readonly LOYALTY_ACCOUNT_NOT_FOUND: { + code: string; + message: string; + documentationLink: string; + }; readonly SECONDARY_TOKEN_EMPTY_BALANCE: { code: string; message: string; @@ -2054,6 +2439,7 @@ export interface PurseHeadlessCheckoutErrorsPayload { method: Method; uniqueness_token: string; }; + LOYALTY_ACCOUNT_NOT_FOUND?: never; SECONDARY_TOKEN_EMPTY_BALANCE?: never; USER_ACTION_REQUEST_HAS_EXPIRED: never; USER_ACTION_REQUEST_ABORTED_BY_CUSTOMER: never; @@ -2218,6 +2604,7 @@ export type DisabledStatePayload = { PAYMENT_ELEMENT_RENDER_FAILED: never; PLUGIN_INCOMPATIBLE: never; PARTNER_UI_FATAL_ERROR: never; + PARTNER_UI_RECOVERABLE_ERROR: never; FATAL_ERROR_BEFORE_RENDER: never; MIN_AMOUNT_NOT_MET: { minAmount: number | null; @@ -2257,6 +2644,11 @@ export type DisabledState = { export declare function disabledStateShouldBeFiltered(state: DisabledState | null | undefined): boolean; export declare function isTechnicallyDisabled(state: DisabledState | null | undefined): boolean; export declare function disabledStateRequiresUIClear(state: DisabledState | null | undefined): boolean; +/** + * The partner reported a failure it can come back from: render the method as + * unavailable, but expect it to become usable again without a page reload. + */ +export declare function isRecoverablyDisabled(state: DisabledState | null | undefined): boolean; export interface CardSchemeAsset { url: string; label: string; @@ -2567,6 +2959,15 @@ export type PurseHeadlessCheckoutSecondaryMethod = PurseHeadlessCheckoutMethod & limit?: number; /** Always true for secondary payment methods */ isSecondary: true; + /** + * Secondary methods (gift cards, vouchers, loyalty) have no payment element — they are + * entered via `getSecondaryToken()` / `take()`, not a rendered form. Calling this warns + * (with a documentation link) and returns `undefined` so the integration degrades + * gracefully instead of throwing a `TypeError`. + * + * @see https://docs.purse.tech/docs/integrate/purse-checkout/headless-checkout/advanced-guides/render-payment-methods#secondary-methods + */ + getPaymentElement(): undefined; } & SecondarySpecificConfig; /** * Represents a primary token in the Purse checkout system. @@ -2668,16 +3069,16 @@ export interface PurseHeadlessCheckoutPrimaryToken extends PurseHeadlessCheckout /** Whether this token is the user's favorite payment method. Reflects `IntegratedToken.favorite`. */ isFavorite: Readable; /** - * Mark this token as the user's favorite. Persists the change via the wallet API. + * Toggle this token's favorite status. If it is already a favorite, it will be removed; + * otherwise it will be marked as the user's favorite. Persists the change via the wallet API. * * @throws - {@link PurseHeadlessCheckoutError} `MISSING_WALLET_SESSION` \ * If no wallet session is loaded - * @throws - {@link PurseHeadlessCheckoutError} `MISSING_TOKEN` \ - * If the token is not found in the wallet * * @example * ```typescript - * await token.setAsFavorite(); + * await token.setAsFavorite(); // marks as favorite + * await token.setAsFavorite(); // removes favorite status * ``` */ setAsFavorite(): Promise; @@ -2703,6 +3104,15 @@ export interface PurseHeadlessCheckoutPrimaryToken extends PurseHeadlessCheckout export interface PurseHeadlessCheckoutSecondaryToken extends PurseHeadlessCheckoutToken { type: "token" | "temporary_token"; isSecondary: true; + /** + * Secondary tokens (gift cards, vouchers, loyalty) have no payment element — they are + * applied via `take()`, not a rendered form. Calling this warns (with a documentation + * link) and returns `undefined` so the integration degrades gracefully instead of + * throwing a `TypeError`. + * + * @see https://docs.purse.tech/docs/integrate/purse-checkout/headless-checkout/advanced-guides/render-payment-methods#secondary-methods + */ + getPaymentElement(): undefined; limit: number; expiration_date: string | Date; /** @@ -2798,6 +3208,7 @@ export type RegistrationState = { registered: boolean; name: string | null; }; +export type SaveTokenMode = "ALWAYS" | "PREFER" | "PREFER_NO" | "NEVER"; export interface PurseHeadlessCheckoutRegisterable { /** * Mark this mean of payment for registration upon validate. @@ -2817,10 +3228,20 @@ export interface PurseHeadlessCheckoutRegisterable { name?: string; }): Promise; /** - * This property exposes the saveToken configuration of the method. - * A mean of payment cannot be saved if the method is not registerable. + * Whether the user can choose to register this method. + * True only for `save_token_mode` values `PREFER` and `PREFER_NO`. + * When false, the save-token checkbox must not be shown; the backend + * enforces the registration policy (ALWAYS/NEVER) without a frontend signal. */ canBeRegistered: boolean; + /** + * The effective `save_token_mode` resolved for this mean of payment + * (`ALWAYS` / `PREFER` / `PREFER_NO` / `NEVER`), including the `save_token` + * boolean fallback. Exposed for consumers that need to distinguish *why* + * registration is or isn't user-controllable — e.g. `ALWAYS` forces + * registration silently, with `canBeRegistered` false like `NEVER`. + */ + saveTokenMode: SaveTokenMode; /** * This property exposes if the mean of payment is bound for registration upon validate. */ @@ -2989,12 +3410,39 @@ export interface PurseHeadlessCheckoutHooks { /** Optional argument to handle the redirection */ redirectionHandler?: (redirectionData: RedirectionHandlerParams) => Promise; } +/** + * How a payment submission was triggered. + * + * - `api`: the merchant called `submitPayment()`. + * - `xpay`: an express/wallet plugin (Apple Pay, Google Pay…) requested validation + * itself, without the merchant calling `submitPayment()`. + */ +export type PurseHeadlessCheckoutSubmitTrigger = "api" | "xpay"; +/** + * Per-attempt detail extracted from a `/validate` response. Kept flat and small: it travels with + * every validation event, including the error ones, so that a failed payment can be diagnosed from + * the log alone without correlating several events. + */ +export interface PurseHeadlessCheckoutValidationResultSummary { + method?: string; + partner?: string; + status: { + state?: string; + code?: string; + description?: string; + }; + /** `plugin_result.status`, the partner-side outcome behind the Purse status code. */ + plugin_status?: string; +} export interface PurseHeadlessCheckoutEventPayload { HEADLESS_CHECKOUT_CLEAR_PRIMARY_SPLIT: { partner: string; method: string; }; - HEADLESS_CHECKOUT_CLIENT_HOOK_ERROR: Record; + HEADLESS_CHECKOUT_CLIENT_HOOK_ERROR: { + error: unknown; + message?: string; + }; HEADLESS_CHECKOUT_CREATE: { sessionType: "v1" | "v2"; screen: { @@ -3088,32 +3536,22 @@ export interface PurseHeadlessCheckoutEventPayload { }; HEADLESS_CHECKOUT_VALIDATION_SUCCESS: { global_status: string; - results: { - method: string; - partner: string; - status: { - state: string; - code: string; - description?: string; - }; - }[]; + results: PurseHeadlessCheckoutValidationResultSummary[]; }; HEADLESS_CHECKOUT_VALIDATION_FAILED: { global_status: string; - results: { - method: string; - partner: string; - status: { - state: string; - code: string; - description?: string; - }; - }[]; + results: PurseHeadlessCheckoutValidationResultSummary[]; }; HEADLESS_CHECKOUT_PAYMENT_ERROR: { message?: string; api_status?: string; stack?: string; + /** Why the payment failed, attempt by attempt. Empty array when the API returned no result. */ + results?: PurseHeadlessCheckoutValidationResultSummary[]; + /** Whether the `/validate` response carried a redirection URL of its own. */ + has_redirection?: boolean; + /** Set when the failure came from a thrown error rather than from an API status. */ + error_name?: string; }; HEADLESS_CHECKOUT_PAYMENT_FULFILLED: { isFulfilled: boolean; @@ -3156,8 +3594,18 @@ export interface PurseHeadlessCheckoutEventPayload { message?: string; stack?: string; }; + /** + * A submission was refused by the re-entrancy guard: one was already in flight, or the session + * had already been validated. Counterpart of widget V3's + * `CHECKOUT_PRE_VALIDATION_ALREADY_ROLLING` / `CHECKOUT_CURRENT_SESSION_ALREADY_VALIDATED`. + */ + HEADLESS_CHECKOUT_SUBMIT_BLOCKED: { + state: PurseHeadlessCheckoutSessionState; + trigger: PurseHeadlessCheckoutSubmitTrigger; + }; HEADLESS_CHECKOUT_SUBMIT_PAYMENT: { splits: PaymentSplit[]; + trigger: PurseHeadlessCheckoutSubmitTrigger; }; HEADLESS_CHECKOUT_TOKEN_DELETE: { partner: string; @@ -3169,7 +3617,16 @@ export interface PurseHeadlessCheckoutEventPayload { method: string; tokenId?: string; }; - HEADLESS_CHECKOUT_UNCAUGHT_ERROR: Record; + HEADLESS_CHECKOUT_TOKEN_SET_FAVORITE: { + partner: string; + method: string; + tokenId?: string; + favorite: boolean; + }; + HEADLESS_CHECKOUT_UNCAUGHT_ERROR: { + stack: string; + message?: string; + }; HEADLESS_CHECKOUT_ON_BEFORE_VALIDATE_HOOK_FAILED: { message: string; error: { @@ -3248,6 +3705,10 @@ export interface Payment { methods: Readable; tokens: Readable; walletTokens: Readable; + primaryMethodElementRegistry: Map; + registration: Writable; + }>; paymentSession: Readable; session: PaymentSession; walletSession: WalletSession | null; @@ -3258,6 +3719,94 @@ export interface Payment { deactivateMop(mop: PurseHeadlessCheckoutPaymentItemBase, reason: DisabledState): Promise; isSessionExpired(): boolean; } +declare class PurseHeadlessCheckoutElement { + private readonly manuallyTriggerHook; + private readonly onSessionTerminated?; + protected readonly item: PurseHeadlessCheckoutPaymentItemBase; + protected readonly payment: Payment; + private fulfilled; + private _wasEverReady; + private readonly registration?; + private readonly amount?; + private readonly interface?; + private readonly env; + protected readonly method: Derived; + private storeListeners; + protected readonly options: Writable; + protected UIListeners: (() => void)[]; + private readonly eventsMap; + ui: PursePartnerUI | null; + constructor(item: PurseHeadlessCheckoutPaymentItemBase, data: { + payment: Payment; + env: APIEnv; + registration?: Readable; + amount?: Readable; + interface?: Interface; + }, manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { + actualMethod?: Method; + sourceId?: string; + }) => Promise, onSessionTerminated?: (() => void) | undefined); + getInterface(): Interface | undefined; + wasEverReady(): boolean; + remove: () => void; + setOptions(options?: Options): void; + hasUI(): boolean; + /** + * This getter is in sync with the "formValid" property of the partner-ui + */ + isFulfilled(): boolean; + /** + * Registers a callback and returns the function that unregisters it. Hosts that + * mount the same element again (dropin remounts its whole list on `setSession`) + * must call it on teardown, otherwise every mount leaves another live callback + * behind and a single `ready` fans out to all of them (SDK-12109). + * + * `remove()` deliberately keeps the map intact: `createPaymentElement` wires the + * splitter and submit plumbing through it once per instance, and the instance is + * cached and reused across remounts, so clearing it here would silently break + * split computation and payment submission. + */ + on(eventName: K, callback: PaymentElementEventsCallback[K]): () => void; + private attachUIListeners; + protected buildUI(): PursePartnerUI; + /** + * Validates the UI of the payment element. + * + * This method triggers the validation process of the UI component associated with the payment element. + * This is purely a form validation function. It can be useful if you want to trigger validation before submitting payment. + * + * @returns {Promise} A promise that resolves with true or the result of the validation + */ + validateUi(): Promise; + private clearListeners; +} +declare class PurseHeadlessCheckoutPaymentElement$1 extends PurseHeadlessCheckoutElement implements PurseHeadlessCheckoutPaymentElement { + appendTo(container: string | Element): void; +} +declare class PurseHeadlessCheckoutHostedFields$1 extends PurseHeadlessCheckoutElement implements PurseHeadlessCheckoutHostedFields { + supportedBrands: Readable; + detectedBrands: Readable; + selectedBrand: Readable; + constructor(item: PurseHeadlessCheckoutPaymentItemBase, data: { + payment: Payment; + env: APIEnv; + registration?: Readable; + amount?: Readable; + interface?: Interface; + }, manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { + actualMethod?: Method; + sourceId?: string; + }) => Promise, onSessionTerminated?: () => void); + protected buildUI(): PursePartnerUI; + setSelectedBrand(brand: CardScheme): void; + setOptions(options?: Options): void; + render(): void; +} +declare function buildPaymentElementAccessors(create: (inter: I) => I extends "form" ? PurseHeadlessCheckoutPaymentElement$1 : PurseHeadlessCheckoutHostedFields$1): { + getPaymentElement: (options?: Omit) => PurseHeadlessCheckoutPaymentElement$1; + getHostedFields: (options?: HostedFieldsOptions) => PurseHeadlessCheckoutHostedFields$1; + getElementInstance: () => PurseHeadlessCheckoutPaymentElement$1 | PurseHeadlessCheckoutHostedFields$1 | undefined; +}; declare class PursePayment implements Payment { readonly factoriesStaticData: { manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { @@ -3278,6 +3827,18 @@ declare class PursePayment implements Payment { methods: Readable; tokens: Readable; walletTokens: Readable; + /** + * Primary-method element cache, keyed by the couple's stable `partner-method` + * id. `setSession` rebuilds `mops` with brand-new item objects on every + * refresh; this registry lets `buildPrimaryPaymentMethod` hand back the same + * `` element (and its `registration` store) for a couple + * whose `configurations` are unchanged, instead of tearing it down — see + * `hasSameMethodConfiguration` usage in `setSession` below. + */ + readonly primaryMethodElementRegistry: Map; + registration: Writable; + }>; constructor(env: APIEnv, factoriesStaticData: { manuallyTriggerHook: (hookName: keyof PurseHeadlessCheckoutHooks, context?: { actualMethod?: Method; @@ -3391,9 +3952,11 @@ export declare class PurseHeadlessCheckout { * - 'idle': The session is idle and not yet initialized. * - 'ready': The session is initialized and ready for payment method selection. * - 'submitting': The payment is being submitted. - * - 'submitted': The payment has been successfully submitted. + * - 'submitted': The session was validated successfully and is now spent. Further submissions + * are refused, since the API accepts one validation per session. * - 'expired': The session has expired. - * - 'error': An error occurred with the session. + * - 'error': An error occurred with the session, including a validation the API refused or a + * payment it declined. The session was not consumed, so submitting again is allowed. * @example * ```ts * headlessCheckout.sessionState.subscribe((state: PurseHeadlessCheckoutSessionState) => { @@ -3423,9 +3986,11 @@ export declare class PurseHeadlessCheckout { * @param {PurseHeadlessCheckoutV2Params | PurseHeadlessCheckoutV1Params} checkoutParams - The checkoutParams data for initializing the checkout. * @param {PurseHeadlessCheckoutHooks} hooks - Optional hooks into checkout lifecycle * @param {APIPaths} apiPaths @internal - dev purposes to override the APi URL's - * @param product @internal - dev purposes to override the product name + * @param wrapper @internal - which product embeds this instance. Set by the wrapping + * product (Dropin passes `dropin`); merchants integrating headless directly leave the + * default. Reported on every log line so events can be attributed to the right product. */ - static init(checkoutParams: PurseHeadlessCheckoutV2Params | PurseHeadlessCheckoutV1Params, hooks?: PurseHeadlessCheckoutHooks, apiPaths?: APIPaths): Promise; + static init(checkoutParams: PurseHeadlessCheckoutV2Params | PurseHeadlessCheckoutV1Params, hooks?: PurseHeadlessCheckoutHooks, apiPaths?: APIPaths, wrapper?: MonitoringWrapper): Promise; /** * Get the config from params * @param {PurseHeadlessCheckoutV2Params | PurseHeadlessCheckoutV1Params} checkoutParams - The checkoutParams data for initializing the checkout. @@ -3506,11 +4071,28 @@ export declare class PurseHeadlessCheckout { */ setSession(widgetData: PurseHeadlessCheckoutV2Params): Promise; setSession(widgetData: PurseHeadlessCheckoutV1Params["paymentSession"]): Promise; + /** + * Shared submission path. Both entry points funnel through here — the public + * `submitPayment()` and express/wallet plugins requesting validation themselves. + * + * The `HEADLESS_CHECKOUT_SUBMIT_PAYMENT` event is emitted here rather than on the + * public method because `withMonitoringProxy` only sees calls made *through* the + * returned proxy. Internal callers hold a reference bound in the constructor, + * before the proxy exists, so monitoring attached to the public method never fires + * for them — which silently lost every wallet submission. + * + * It is emitted before the session and fulfilment guards on purpose: the event + * means "a submission was attempted", so guard failures stay visible. Pair it with + * `HEADLESS_CHECKOUT_PAYMENT_ERROR` to see which attempts were rejected. + */ private _submitPayment; /** * Submits the current payment configuration for processing. * This will validate and process all payment methods in the current split configuration. * + * Resolves without doing anything when a submission is already in flight, or when the session + * has already been validated successfully. Watch `sessionState` to tell the two apart. + * * @throws * - {@link PurseHeadlessCheckoutError} `MISSING_SESSION` \ * If the session is missing @@ -3589,7 +4171,12 @@ export type HeadlessCheckout = PurseHeadlessCheckout; * * @returns A promise that resolves to a configured checkout instance */ -export declare const createHeadlessCheckout: (sessionParams: T, hooks?: H, apiPaths?: APIPaths) => Promise; +export declare const createHeadlessCheckout: (sessionParams: T, hooks?: H, apiPaths?: APIPaths, +/** + * @internal Which product embeds this instance — see {@link PurseHeadlessCheckout.init}. + * Merchants integrating headless directly should not pass this. + */ +wrapper?: MonitoringWrapper) => Promise; export { HostedFormUIOptions as PurseHeadlessCheckoutHostedFormUIOptions, diff --git a/src/generated/types/securefields.d.ts b/src/generated/types/securefields.d.ts index fc3d352..01d748d 100644 --- a/src/generated/types/securefields.d.ts +++ b/src/generated/types/securefields.d.ts @@ -50,7 +50,7 @@ export interface SubmitOptions { cardHolderName?: string; } /** - * Standard / PCI Proxy tokenization — `vault_form_token` is always present; + * Standard tokenization — `vault_form_token` is always present; * `card` is optional; `birth_date` is never present. * @category Core */ @@ -116,6 +116,14 @@ export type SubmitResultError = { headers?: { "Cf-Ray": string | null; }; + /** Per-field snapshot, no card values. Attached only when the submit was blocked before the API call. */ + fields?: Partial>; }; /** * Union of all possible success variants. @@ -181,12 +189,14 @@ export interface HostedFormUIOptions { cvvPlaceholder?: string; cvvInputLabel?: string; /** - * Accepted and ignored. In the widget these only reach Adyen's own card component, which owns the - * 3 vs 4 digit switch; the Purse hosted fields path labels a 4 digit code "CVV" like any other, and - * Secure Fields matches it. + * Placeholder of the cvv slot while the detected brand asks for a 4 digit code (American Express). + * Falls back to `cvvPlaceholder` when the merchant set that one and not this, then to `1234`. */ cvv4Placeholder?: string; - /** Accepted and ignored — see {@link HostedFormUIOptions.cvv4Placeholder}. */ + /** + * Label — and accessible name — of the cvv slot while the detected brand asks for a 4 digit code. + * Falls back to `cvvInputLabel`: a 4 digit code is still a CVV, so the same name is usually right. + */ cvv4InputLabel?: string; cvvRequiredError?: string; /** @deprecated use cvvRequiredError */ @@ -446,25 +456,32 @@ export interface SecureFieldsEventsPayload { * Triggered when the submission has been successful. * * Three mutually exclusive cases: - * - **Standard / PCI Proxy card**: `vault_form_token` is present; `card` is optionally present; + * - **Standard card**: `vault_form_token` is present; `card` is optionally present; * `birth_date` is absent. * - **Oney full-form**: both `vault_form_token` and `birth_date` are present; `card` is absent. * - **Oney CVV-only**: only `birth_date` is present; `vault_form_token` and `card` are absent. * * Use `'vault_form_token' in payload` or `'birth_date' in payload` to narrow the variant. + * + * With 3DS enabled at init, this fires only once the 3DS sequence has run, and carries its + * `three_ds_server_trans_id` — a submit whose 3DS could not be attempted emits `error`, never `success`. */ success: { - /** Standard or PCI Proxy tokenization — always present in this variant. */ + /** Standard tokenization — always present in this variant. */ vault_form_token: string; /** Card metadata returned by the backend. Present when the backend supplies it. */ card?: CardInfo; birth_date?: never; + /** Present when 3DS is enabled: correlates every 3DS step of this authentication. */ + three_ds_server_trans_id?: string; } | { /** Vault token issued for the Oney full-form path. */ vault_form_token: string; card?: never; /** ISO 8601 date of birth required by Oney (`YYYY-MM-DD`). */ birth_date: string; + /** Present when 3DS is enabled: correlates every 3DS step of this authentication. */ + three_ds_server_trans_id?: string; } | { vault_form_token?: never; card?: never; @@ -617,8 +634,54 @@ export interface SecureFieldsFieldConfig { */ brandLabels?: Partial>; } +export declare const Environments: readonly [ + "sandbox", + "production" +]; +export type Environment = (typeof Environments)[number]; +declare const AllowedFields: string[]; +export type AllowedFields = (typeof AllowedFields)[number]; +export declare const SecureFieldsModes: readonly [ + "hosted_fields", + "hosted_form" +]; +export type SecureFieldsMode = (typeof SecureFieldsModes)[number]; +/** + * Turns the built-in 3DS sequence on. + * + * With `enabled: true`, {@link SecureFieldsClient.submit} chains the 3DS steps after tokenization — + * the versioning call, then the 3DS Method (device fingerprint) in a hidden iframe when the card + * range has one — and resolves with `three_ds_server_trans_id` on top of the usual tokenization result. + * + * The chain cannot run at init: it needs the form token, which only exists once the card form has + * been tokenized. This flag only arms it. + * + * @category Configuration + */ +export interface SecureFieldsThreeDSConfig { + /** Off by default, so an existing integration is unaffected. */ + enabled: boolean; +} +/** + * What {@link SecureFieldsClient.submit} adds to the tokenization result when 3DS is enabled. + * + * Forward it, with the form token, to your own backend: it goes in the payment creation call, next to + * the browser information from {@link getBrowserData} and the `browserIP` / `Accept` header the + * backend captures from the cardholder's request itself. + * + * @category Core + */ +export interface ThreeDSSubmitData { + /** Correlates every 3DS step of this authentication. */ + three_ds_server_trans_id?: string; +} +/** + * The tokenization result, plus the 3DS data when 3DS is enabled at init. + * @category Core + */ +export type SecureFieldsSubmitResult = SubmitResult & ThreeDSSubmitData; /** - * Configuration for the Vault SDK Adapter. + * Configuration accepted by {@link initSecureFields}. * * There are two modes, discriminated by `mode`: * - {@link SecureFieldsHostedFieldsConfig} (`'hosted_fields'`, the default) — you place and lay out @@ -695,6 +758,10 @@ export interface SecureFieldsHostedFieldsConfig { [key in CSSPseudoClasses]?: SecureFieldsStyles; }; }; + /** + * Runs the 3DS sequence inside `submit()`. See {@link SecureFieldsThreeDSConfig}. + */ + threeDS?: SecureFieldsThreeDSConfig; } /** * Hosted form mode: the SDK renders a complete, ready to use card form — card number, expiry date, CVV @@ -769,9 +836,35 @@ export interface SecureFieldsHostedFormConfig { * still resolves across the mode union. */ styles?: never; + /** + * Runs the 3DS sequence inside `submit()`. See {@link SecureFieldsThreeDSConfig}. + */ + threeDS?: SecureFieldsThreeDSConfig; } /** - * Common interface for all vault SDK adapters. + * Partial update for {@link SecureFieldsConfig}. + * Only the specified keys are updated; everything else is left as it is. + * @category Configuration + */ +export type SecureFieldsConfigUpdate = Partial<{ + /** + * Per-field texts. Targets cannot be patched — the fields do not move. + */ + fields: Partial<{ + [key in AllowedFields]: Omit; + }>; + styles: SecureFieldsHostedFieldsConfig["styles"]; + /** + * Hosted form mode only: replaces the displayed texts. + */ + hostedForm: HostedFormUIOptions; + /** + * Hosted form mode only: replaces the theme. + */ + theme: SecureFieldsTheme; +}>; +/** + * The client returned by {@link initSecureFields}. * @category Core */ export interface SecureFieldsClient { @@ -830,6 +923,13 @@ export interface SecureFieldsClient { * * } * ``` * + * With `threeDS: { enabled: true }` in the init config, the same call also runs the 3DS sequence + * after tokenization — versioning, then the 3DS Method in a hidden iframe when the card range has + * one — and the result carries the `three_ds_server_trans_id` your backend needs for the payment + * creation call, alongside the browser information you collect with `getBrowserData()`. + * Fingerprinting is best-effort and never blocks the submit; a failed versioning call does, since + * 3DS cannot be attempted without it. + * * @see {@link SecureFieldsErrors} * * ### Events @@ -838,7 +938,7 @@ export interface SecureFieldsClient { * * `error` Emitted when there is an error during the tokenization process. */ - submit: (payload?: SubmitOptions) => Promise; + submit: (payload?: SubmitOptions) => Promise; /** * Tells the card fields which brand was chosen for a co-badged card. * @@ -875,14 +975,6 @@ export interface SecureFieldsClient { */ on(event: K, callback: (data: SecureFieldsEventsPayload[K]) => void): void; } -declare const VAULT_VENDORS: readonly [ - "purse", - "pci_proxy" -]; -/** - * @hidden - */ -export type Vendor = (typeof VAULT_VENDORS)[number]; /** * Error codes and messages for the Secure Fields SDK. * @category Errors @@ -894,87 +986,87 @@ export declare const SecureFieldsErrors: { readonly TENANT_ID_REQUIRED: { readonly code: "TENANT_ID_REQUIRED"; readonly message: "Tenant ID is required for Secure Fields initialization"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/#prerequisites"; - }; - readonly TENANT_ID_FORMAT_MISMATCH_VENDOR: { - readonly code: "TENANT_ID_FORMAT_MISMATCH_VENDOR"; - readonly message: "Tenant ID format mismatch the one implied by vendor"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/#prerequisites"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/#prerequisites"; }; readonly INVALID_BRANDS: { readonly code: "INVALID_BRANDS"; readonly message: "One or more specified card brands are not supported"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#brands"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#brands"; }; readonly NOT_SUPPORTED_FIELD: { readonly code: "NOT_SUPPORTED_FIELD"; readonly message: "The specified field type is not supported"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#fields"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#fields"; }; readonly FIELD_CONFIG_REQUIRED: { readonly code: "FIELD_CONFIG_REQUIRED"; readonly message: "Fields configuration is mandatory for SDK initialization"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#fields"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Configuration/SecureFieldsConfig#fields"; }; readonly FIELD_TARGET_INVALID: { readonly code: "FIELD_TARGET_INVALID"; readonly message: "Fields target must be valid DOM element IDs"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Configuration/SecureFieldsFieldConfig#target"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Configuration/SecureFieldsFieldConfig#target"; }; readonly INIT_FAILED: { readonly code: "INIT_FAILED"; readonly message: "SDK initialization failed"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly FIELD_RENDER_FAILED: { readonly code: "FIELD_RENDER_FAILED"; readonly message: "Failed to render one or more fields"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly FIELDS_NOT_RENDERED: { readonly code: "FIELDS_NOT_RENDERED"; readonly message: "Fields must be rendered before performing this action"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; }; readonly INVALID_FORM: { readonly code: "INVALID_FORM"; readonly message: "Form validation failed - check field values"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; }; readonly TOKENIZATION_FAILED: { readonly code: "TOKENIZATION_FAILED"; readonly message: "Failed to tokenize payment information"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Core/SecureFieldsClient#submit"; + }; + readonly THREEDS_VERSIONING_FAILED: { + readonly code: "THREEDS_VERSIONING_FAILED"; + readonly message: "The 3DS versioning call failed - 3DS cannot be attempted"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly NETWORK_ERROR: { readonly code: "NETWORK_ERROR"; readonly message: "Network request failed - check your connection"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly INSTANCE_DESTROYED: { readonly code: "INSTANCE_DESTROYED"; readonly message: "Instance has been destroyed and cannot be used"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly UNEXPECTED_ERROR: { readonly code: "UNEXPECTED_ERROR"; readonly message: "An unexpected error occurred"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly THREEDS_INVALID_URL: { readonly code: "THREEDS_INVALID_URL"; readonly message: "The 3DS endpoint must be a valid http(s) URL"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly THREEDS_INVALID_INPUT: { readonly code: "THREEDS_INVALID_INPUT"; readonly message: "Invalid 3DS parameters"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/"; }; readonly CONTAINER_NOT_FOUND: { readonly code: "CONTAINER_NOT_FOUND"; readonly message: "Container element not found in the DOM"; - readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-checkout/advanced-flow/sdk-references/Configuration/SecureFieldsFieldConfig#target"; + readonly documentationLink: "https://docs.purse.tech/docs/integrate/purse-api/additional-features/advanced-flow/sdk-references/Configuration/SecureFieldsFieldConfig#target"; }; }; /** @@ -1120,20 +1212,42 @@ export interface ThreeDSFingerprintOptions extends ThreeDSFrameOptionsBase { /** Hard stop, in milliseconds. Defaults to `10_000` — the EMVCo 3DS Method rule. */ timeoutMs?: number; /** - * How many navigations of the iframe count as "done". - * - * The EMVCo 3DS Method navigates twice: the ACS fingerprinting page, then its auto-submitted form - * to the notification URL. Hence the default of `2`. Set `1` for a provider whose collection - * endpoint answers in a single response. + * How many navigations of the iframe count as "done". Defaults to `1`, the ACS fingerprinting + * page: its POST to the notification URL may never surface as a load event here, so counting it + * would only spend the timeout. Set `2` for an ACS that navigates the frame itself. */ resolveAfterLoads?: number; } +/** + * The `browser` node of `POST /v2/payments`: snake_case, integer numerics. Produced by + * {@link toPaymentBrowserNode}. `referrer`, `accept_header`, `user_agent` and `accept_language` are + * absent — those are read from the request server-side. + * + * @category 3DS + */ +export interface PaymentBrowserNode { + java_enabled: boolean; + javascript_enabled: boolean; + /** Browser language as a BCP 47 tag, e.g. `fr-FR`. */ + locale: string; + color_depth: number; + screen_height: number; + screen_width: number; + /** Offset between UTC and local time, in minutes. */ + utc_time_zone: number; +} /** * Options for {@link threeDSChallenge}. * * @category 3DS */ export interface ThreeDSChallengeOptions extends ThreeDSFrameOptionsBase { + /** + * The opaque `challenge_data` blob from the `POST /v2/payments` answer, forwarded verbatim. + * Supplies `acsUrl`, `creq` and `challengeWindowSize`. Explicit options override it, except the + * window size: the blob's value is the one the AReq announced, so it wins. + */ + challengeData?: string; /** EMVCo alias for `url` — the issuer ACS challenge endpoint from the authentication response. */ acsUrl?: string; /** EMVCo alias for `fields` — posted as a single `creq` field. */ @@ -1144,19 +1258,27 @@ export interface ThreeDSChallengeOptions extends ThreeDSFrameOptionsBase { */ container: string | HTMLElement; /** - * Iframe size, as the EMVCo code. Defaults to `05` (fills the container). + * Iframe size, as the EMVCo code, for a call that carries no `challengeData`. Defaults to `05` + * (fills the container). * - * Pass through the value the authentication response echoed back: EMVCo requires the rendered size - * to match what was announced in the AReq, so this is not a client-side choice. + * The size is an AReq echo, not a client-side choice — EMVCo requires the rendered frame to match + * what was announced — so a `challengeWindowSize` inside the blob wins over this one. */ challengeWindowSize?: ChallengeWindowSize; - /** Explicit CSS size, for a provider that does not use the EMVCo codes. Overrides the code. */ - size?: { - width: string; - height: string; - }; - /** Required: a challenge has no other way to report that the cardholder is done. */ - completion: ThreeDSCompletion; + /** + * How the ACS reports that the cardholder is done. Required unless `resolveAfterLoads` is set — + * a challenge has no other way to report a result. + */ + completion?: ThreeDSCompletion; + /** + * Settle after this many navigations of the iframe instead of on a message. + * + * For a provider whose result page is a plain navigation rather than a `postMessage`, this is the + * only signal there is — the Purse 3DS challenge posts one, so use `completion` there. The + * about:blank document the iframe starts on is not counted, so `1` means the first real + * navigation. Can be combined with `completion`: whichever fires first settles the challenge. + */ + resolveAfterLoads?: number; /** * Hard stop, in milliseconds. Defaults to `600_000` (10 min) — long enough for an OTP or a banking * app confirmation, short enough that a silent ACS eventually releases the checkout. @@ -1239,8 +1361,11 @@ export declare const threeDSFingerprint: (options: ThreeDSFingerprintOptions) => * EMVCo `challengeWindowSize` the authentication response echoed back. The SDK adds no overlay, no * dialog and no styling of its own — the surrounding UI is yours. * - * Completion is a `postMessage` from the page the flow lands on, which is why `completion.origins` is - * required: only those exact origins can end the wait. A message from anywhere else is ignored. + * Two ways to know the cardholder is done, and a call needs at least one. `completion` waits for a + * `postMessage` from the page the flow lands on, which is why `completion.origins` is required — + * only those exact origins can end the wait — the Purse 3DS challenge included: its notification + * endpoint answers with a page that posts the completion. `resolveAfterLoads` waits for the iframe to + * navigate instead, for a provider whose result page posts nothing. * * Note the challenge outcome is authoritative **server-side**. Whatever this resolves with, confirm * the payment through your backend before fulfilling: an abandoned or timed-out challenge is settled @@ -1256,6 +1381,22 @@ export declare const threeDSFingerprint: (options: ThreeDSFingerprintOptions) => * | THREEDS_INVALID_INPUT | * | CONTAINER_NOT_FOUND | * + * @example Purse 3DS, from the `authentication.challenge_data` blob of the `POST /v2/payments` answer + * ```ts + * const { status, data } = await threeDSChallenge({ + * challengeData, // forwarded verbatim from your backend + * container: 'checkout-3ds-slot', + * completion: { + * // The Purse API origin your payments are created against — ask Purse for the one matching + * // your environment. A message from any other origin is ignored. + * origins: [PURSE_API_ORIGIN], + * match: (data) => (data as { type?: string }).type === 'purse:3ds:challenge-completed', + * }, + * }); + * // `data` carries `result` (`SETTLED` | `PENDING` | `REJECTED`) and `payment_id`; confirm the + * // payment through your backend either way. + * ``` + * * @example EMVCo challenge, from the authentication response * ```ts * const { status, data } = await threeDSChallenge({ @@ -1273,7 +1414,6 @@ export declare const threeDSFingerprint: (options: ThreeDSFingerprintOptions) => * url: stepUpUrl, * fields: { JWT: accessToken, MD: merchantData }, * container: challengeSlot, - * size: { width: '100%', height: '600px' }, * completion: { * origins: [window.location.origin], * match: (data) => (data as { MessageType?: string }).MessageType === 'stepUpComplete', @@ -1292,18 +1432,45 @@ export declare const threeDSChallenge: (options: ThreeDSChallengeOptions) => Pro * are filled in server-side from the request itself. Pass the result to your backend along with the * vault form token so it can build the authentication request. * + * Field names are the EMVCo ones — for the `browser` node of `POST /v2/payments`, map them with + * {@link toPaymentBrowserNode}. + * * @example * ```ts * const { vault_form_token } = await secureFields.submit(); * await fetch('/checkout/authenticate', { * method: 'POST', - * body: JSON.stringify({ vault_form_token, browserData: getBrowserData() }), + * body: JSON.stringify({ vault_form_token, browser: toPaymentBrowserNode() }), * }); * ``` * * @category 3DS */ export declare const getBrowserData: () => ThreeDSBrowserData; +/** + * Maps {@link getBrowserData} onto the `browser` node of `POST /v2/payments`, which takes + * snake_case keys and integers where EMVCo uses `browser*` and strings. + * + * `referrer`, `accept_header`, `user_agent` and `accept_language` are left out: they come from the + * request your backend receives, not from the page. + * + * @example + * ```ts + * const { vault_form_token, three_ds_server_trans_id } = await secureFields.submit(); + * await fetch('/checkout/pay', { + * method: 'POST', + * body: JSON.stringify({ + * vault_form_token, + * three_ds_server_trans_id, + * browser: toPaymentBrowserNode(), + * }), + * }); + * ``` + * + * @param data Browser data to map. Collected with {@link getBrowserData} when omitted. + * @category 3DS + */ +export declare const toPaymentBrowserNode: (data?: ThreeDSBrowserData) => PaymentBrowserNode; /** * @function * Initializes the Secure Fields SDK with the provided tenant ID and configuration. @@ -1335,7 +1502,6 @@ export declare const initSecureFields: (sdkConfiguration: { * Configuration for the secure fields. See {@link SecureFieldsConfig} for supported options. */ config: SecureFieldsConfig; - vault_vendor?: Vendor; }) => Promise; export {};