# Click-once

Learn how to configure the click-once component.

## Basic usage

### Minimal configuration

At minimum, initialise checkout, then create click-once with nested submit callbacks so authorisation can complete. List token retrieval needs `ownerType`, `ownerId`, and a shopper ID from `onGetShopper` on checkout initialisation, as on other card-on-file flows. Pre-bound `gatewayTokenId` or `externalTokenId` still needs `ownerType` and `ownerId` on `PxpCheckout.initialize`, but doesn't use `onGetShopper`. Click-once config alone doesn't replace that checkout setup.

```typescript
import PxpCheckout from '@pxp-io/web-components-sdk';

const pxpCheckout = PxpCheckout.initialize({
  session: {
    sessionId: 'your-session-id',
    hmacKey: 'your-hmac-key',
    encryptionKey: 'your-encryption-key'
  },
  environment: 'production',
  transactionData: {
    amount: 1000,
    currency: 'USD',
    entryType: 'Ecom',
    intent: {
      card: 'Authorisation'
    },
    merchantTransactionId: 'unique-transaction-id',
    merchantTransactionDate: () => new Date().toISOString()
  },
  kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
  ownerType: 'MerchantGroup',
  ownerId: 'your-owner-id',
  onGetShopper: async () => ({ id: 'shopper-id' })
});

const clickOnceComponent = pxpCheckout.create('click-once', {
  cardSubmitComponentConfig: {
    onPreAuthorisation: async () => ({}),
    onPostAuthorisation: (result) => {
      console.log('Payment completed:', result.merchantTransactionId);
    },
  },
});

clickOnceComponent.mount('click-once-container');
```

`onPreAuthorisation` may return `{}` when you don't need extra PSD2 or risk data. For payouts, return `riskScreeningData` where required. See the payout example on this page.

### Advanced configuration

For more complex implementations, you can configure filtering, sorting, nested components, and event handlers:

```typescript
const clickOnceComponent = pxpCheckout.create('click-once', {
  // Token management
  limitTokens: 5,
  filterBy: {
    excludeExpiredTokens: true,
    schemes: ["Visa", "Mastercard"],
    fundingSource: "Credit"
  },
  orderBy: {
    lastUsageDate: { 
      direction: "desc",
      orderByField: "lastSuccessfulPurchaseDate",
      priority: 1
    }
  },
  
  // CVC requirement
  isCvcRequired: true,
  cvcComponentConfig: {
    required: true,
    label: "Security Code",
    applyMask: true,
    showMaskToggle: true
  },
  
  // Submit configuration
  cardSubmitComponentConfig: {
    submitText: "Pay {amount} {currency}",
    onPreAuthorisation: async (data) => {
      return { psd2Data: {} };
    },
    onPostTokenisation: (data) => {
      // Store data.gatewayTokenId on your backend. Pass it as gatewayTokenId on a later
      // click-once config so the shopper pays with that token without selecting it.
      saveGatewayTokenId(data.gatewayTokenId);
    },
    onPostAuthorisation: (result) => {
      console.log('Payment completed:', result.merchantTransactionId);
      window.location.href = '/success';
    }
  },
  
  // Event handlers
  onRetrieveTokensFailed: (error) => {
    console.error('Failed to retrieve tokens:', error);
    showErrorMessage('Unable to load saved cards. Please try again.');
  }
});

clickOnceComponent.mount('click-once-container');
```

## Styling

```typescript
const clickOnceConfig: ClickOnceComponentConfig = {
  limitTokens: number,
  filterBy: {
    excludeExpiredTokens: boolean,
    schemes: string[],
    fundingSource: string,
    issuerCountryCode: string,
    ownerType: string
  },
  orderBy: {
    expiryDate: {
      direction: string,
      priority: number
    },
    scheme: {
      valuesOrder: string[],
      priority: number
    },
    fundingSource: {
      valuesOrder: string[],
      priority: number
    },
    ownerType: {
      valuesOrder: string[],
      priority: number
    },
    issuerCountryCode: {
      direction: string,
      priority: number
    },
    lastUsageDate: {
      direction: string,
      orderByField: string,
      priority: number
    }
  },
  cardBrandImages: {
    visaSrc: string,
    mastercardSrc: string,
    amexSrc: string,
    cupSrc: string,
    dinersSrc: string,
    discoverSrc: string,
    jcbSrc: string
  },
  isCvcRequired: boolean,
  cvcComponentConfig: CardCvcComponentConfig,
  cardSubmitComponentConfig: CardSubmitComponentConfig,
  hideCardBrandLogo: boolean,
  submitText: string,
  disableCardSelection: boolean,
  isRenderLastPurchaseCard: boolean,
  isRenderLastPayoutCard: boolean,
  class: string,
  selectTokenItemClass: string,
  useTransparentCardBrandImage: boolean,
  selectCardButtonAriaLabel: string,
  label: string,
  labelAriaLabel: string,
  submitAriaLabel: string,
  cardNumberAriaLabel: string,
  cardExpiryDateAriaLabel: string,
  transactionInitiatorType: TransactionInitiatorType,
  gatewayTokenId: string,
  externalTokenId: string
}
```

| Parameter | Description |
|  --- | --- |
| `limitTokens`number | The maximum number of tokens to display in the component. |
| `filterBy`object | Details about the filtering options. Applies to list token retrieval only. Pre-bound `gatewayTokenId` or `externalTokenId` skips this filtering (expired cards, schemes, and similar). |
| `filterBy.excludeExpiredTokens`boolean     - Whether to exclude tokens associated with expired cards. |
| `filterBy.schemes`string [] | The list of card schemes to include. For example, `[Visa, Mastercard]`. |
| `filterBy.fundingSource`string | The funding source type to include. For example `Credit` or `Debit`. |
| `filterBy.issuerCountryCode`string | The issuer country code to include. |
| `filterBy.ownerType`string | The owner type to include. |
| `orderBy`object | Details about the ordering options. |
| `orderBy.expiryDate`object | Details for ordering by expiry date. |
| `orderBy.expiryDate.direction`string | The direction to order by.Possible values:`desc``asc` |
| `orderBy.expiryDate.priority`number | The priority of the ordering option. |
| `orderBy.scheme`object | Details for ordering by card scheme. |
| `orderBy.scheme.valuesOrder`string [] | The ordered list of card schemes. For example, `['Visa', 'Mastercard', 'Amex']`. |
| `orderBy.scheme.priority`number | The priority of the ordering option. |
| `orderBy.fundingSource`object | Details for ordering by funding source. |
| `orderBy.fundingSource.valuesOrder`string [] | The ordered list of funding sources.Possible values:CreditDebit |
| `orderBy.fundingSource.priority`number | The priority of the ordering option. |
| `orderBy.ownerType`object | Details for ordering by owner type. |
| `orderBy.ownerType.valuesOrder`string [] | The ordered list of owner types.Possible values:`Consumer``Commercial` |
| `orderBy.ownerType.priority`number | The priority of the ordering option. |
| `orderBy.issuerCountryCode`object | Details for ordering by issuer country code. |
| `orderBy.issuerCountryCode.direction`string- The direction to order by.Possible values:`desc``asc` |
| `orderBy.lastUsageDate`object | Details for ordering by last usage date. |
| `orderBy.lastUsageDate.direction`string | The direction to order by.Possible values:`desc``asc` |
| `cardBrandImages`object | Details about the card brand images. |
| `cardBrandImages.visaSrc`string | The URL for the Visa card brand image. |
| `cardBrandImages.mastercardSrc`string | The URL for the Mastercard card brand image. |
| `cardBrandImages.amexSrc`string | The URL for the Amex card brand image. |
| `cardBrandImages.cupSrc`string | The URL for the CUP card brand image. |
| `cardBrandImages.dinersSrc`string | The URL for the Diners card brand image. |
| `cardBrandImages.discoverSrc`string | The URL for the Discover card brand image. |
| `cardBrandImages.jcbSrc`string | The URL for the JCB card brand image. |
| `isCvcRequired`boolean | Whether CVC is required. Omit this property or set `true` to require CVC. Set `false` to disable the component-level requirement. When you use `onPreRenderTokens`, per-token `CardTokenMapping.isCvcRequired` applies instead. |
| `cvcComponentConfig`CardCvcComponentConfig | Details about the configuration for the card CVC component. See [Card CVC](/guides/checkout/components/web/card/card-cvc). |
| `cardSubmitComponentConfig`CardSubmitComponentConfig | Details about the configuration for the card submit component. See [Card submit](/guides/checkout/components/web/card/card-submit). |
| `hideCardBrandLogo`boolean | Whether to hide the card brand logo. |
| `submitText`string | The text for the built-in pay or withdraw button. If unset and `transactionData.intent.card` is `IntentType.Payout`, the SDK uses the default withdraw wording. |
| `disableCardSelection`boolean | Whether to disable card selection. |
| `isRenderLastPurchaseCard`boolean | Whether to render the last card that was used for a purchase. |
| `isRenderLastPayoutCard`boolean | Optional. Set `true` for payout-heavy last-card selection, as in the payout example. Leave unset for default last-activity behaviour across purchase and payout dates. |
| `class`string | The class name for the component. |
| `selectTokenItemClass`string | The class name for the select token item in the select token list. |
| `useTransparentCardBrandImage`boolean | Whether to use transparent card brand images. Defaults to `true`. |
| `selectCardButtonAriaLabel`string | The aria label for the select card button. |
| `label`string | The label for the click-once component. |
| `labelAriaLabel`string | The aria label for the click-once component. |
| `submitAriaLabel`string | The aria label for the submit button. |
| `cardNumberAriaLabel`string | The aria label for the card number. |
| `cardExpiryDateAriaLabel`string | The aria label for the card expiry date. |
| `transactionInitiatorType`TransactionInitiatorType | Optional. Use `CIT` for customer-initiated payments and `MIT` for merchant-initiated payments. For list load, `CIT` with a non-payout card intent requests PAN (card) tokens only. Payout intent does not apply that filter. Pre-bound `gatewayTokenId` or `externalTokenId` load is unaffected. |
| `gatewayTokenId`string | The gateway token ID of a saved card. When set, click-once loads that token directly and the shopper doesn't select a card from the list. Store the value from `onPostTokenisation` (for example on a new-card submit) and pass it here on a later click-once checkout. If both `gatewayTokenId` and `externalTokenId` are set, `gatewayTokenId` takes precedence. |
| `externalTokenId`string | External token identifier. Use this to load a specific token when you have a token ID from an external system and aren't using `gatewayTokenId`. |


The `cardSubmitComponentConfig` property accepts all card submit component configurations, including the `onCustomValidation` callback for validating merchant-owned fields alongside SDK component validation. See [Card submit](/guides/checkout/components/web/card/card-submit) for all available submit configuration options.

**Payout support:** The click-once component supports card payouts (also called disbursements or withdrawals) where funds are sent from your merchant account to a cardholder's card. This is commonly used for:

- Marketplace seller payments
- Insurance claim settlements
- Refunds and reimbursements
- Competition prizes and rewards


When using `intent: { card: IntentType.Payout }`, configure `isRenderLastPayoutCard: true` to display cards previously used for successful payouts, providing a familiar payout experience for returning recipients.

## Event handling

Click-once supports these callbacks on `ClickOnceComponentConfig`:

```typescript
const clickOnceConfig: ClickOnceComponentConfig = {
  onOnceCardClick: () => void,
  onPreRenderTokens: (data: RetrieveCardTokensReponseSuccess) => CardTokenMapping[],
  onRetrieveTokensFailed: (error: BaseSdkException | RetrieveCardTokensReponseFailed | RetrieveCardTokenDetailsResponseFailed) => void,
  onRetrieveTokensSuccess: (data: RetrieveCardTokensReponseSuccess | RetrieveCardTokenDetailsResponseSuccess) => void,
  onLoadIframeFailed: (error: FieldLoadFailedException) => void,
  buttonBuilder: (elementIds: ClickOnceButtonBuilderElementIds) => string,
  selectTokenItemBuilder: (elementIds: ClickOnceSelectTokenBuilderElementIds) => string
};
```

| Callback | Description |
|  --- | --- |
| `onOnceCardClick: () => void` | Event handler for when a card is clicked. |
| `onPreRenderTokens: (data: RetrieveCardTokensReponseSuccess) => CardTokenMapping[]` | Callback to order or filter tokens before rendering. Receives the successful response from the token retrieval API and returns an array of transformed card token objects ready for display. |
| `onRetrieveTokensFailed: (error: BaseSdkException \| RetrieveCardTokensReponseFailed \| RetrieveCardTokenDetailsResponseFailed) => void` | Event handler for when token retrieval fails. Receives error details for handling and displaying appropriate error messages. |
| `onRetrieveTokensSuccess: (data: RetrieveCardTokensReponseSuccess \| RetrieveCardTokenDetailsResponseSuccess) => void` | Event handler for when tokens are retrieved successfully. |
| `onLoadIframeFailed: (error: FieldLoadFailedException) => void` | Event handler for when iframe loading fails. |
| `buttonBuilder: (elementIds: ClickOnceButtonBuilderElementIds) => string` | Callback to build a custom token item layout. Use the ids from `ClickOnceButtonBuilderElementIds`: `tokenImageId`, `cardNumberId`, `cvcComponentId`, and `payNowId`. |
| `selectTokenItemBuilder: (elementIds: ClickOnceSelectTokenBuilderElementIds) => string` | Callback to build custom token label. Receives element IDs for the token and returns custom HTML for displaying the token selection item. |


To react to CVC input, set `onChange` or validation callbacks on `cvcComponentConfig`. See [Card CVC](/guides/checkout/components/web/card/card-cvc).

For more information about callbacks, see [Events](/guides/checkout/components/web/card/events).

## Examples

### Pay with a stored gateway token

Use `gatewayTokenId` when you already know which saved card to charge. Capture the ID from `onPostTokenisation` when the card is first tokenised, then pass it into click-once. Click-once loads that token directly, so the shopper doesn't select a card from the list. Set `ownerType` and `ownerId` on `PxpCheckout.initialize` for this path, the same as for list retrieval. You can omit `onGetShopper` until you load a token list. Pre-bound `gatewayTokenId` or `externalTokenId` skips list filtering such as expired cards and schemes.

```typescript
// First payment / new card: store the gateway token ID after tokenisation
const cardSubmit = pxpCheckout.create('card-submit', {
  // ...newCardComponent and other submit config
  onPostTokenisation: (data) => {
    // Store data.gatewayTokenId on your backend. Pass it as gatewayTokenId on a later
    // click-once config so the shopper pays with that token without selecting it.
    saveGatewayTokenId(data.gatewayTokenId);
  },
  onPostAuthorisation: (result) => {
    console.log('Payment completed:', result.merchantTransactionId);
  }
});

// Later checkout: charge that token without card selection
const storedGatewayTokenId = await getStoredGatewayTokenId(); // e.g. '123123-123123-123123-123123'

const clickOnceComponent = pxpCheckout.create('click-once', {
  gatewayTokenId: storedGatewayTokenId,
  isCvcRequired: true,
  cvcComponentConfig: {
    required: true,
    label: 'Security Code'
  },
  cardSubmitComponentConfig: {
    submitText: 'Pay {amount} {currency}',
    onPreAuthorisation: async () => ({}),
    onPostAuthorisation: (result) => {
      console.log('Payment completed:', result.merchantTransactionId);
      window.location.href = '/success';
    }
  }
});

clickOnceComponent.mount('click-once-container');
```

### Payment flow

A complete implementation with filtering, sorting, and integrated payment handling:

```typescript
const clickOnceConfig: ClickOnceComponentConfig = {
 limitTokens: 5,
  filterBy: {
    excludeExpiredTokens: true,
    schemes: ["Visa", "Mastercard"],
    fundingSource: "Credit",
    issuerCountryCode: "USA",
    ownerType: "Consumer"
  },
  orderBy: {
    expiryDate: { 
      direction: "asc",
      priority: 1
    },
    scheme: { 
      valuesOrder: ["Visa", "Mastercard", "Amex"],
      priority: 2
    },
    fundingSource: { 
      valuesOrder: ["Credit", "Debit"],
      priority: 3
    },
    ownerType: { 
      valuesOrder: ["Consumer", "Commercial"],
      priority: 4
    },
    issuerCountryCode: { 
      direction: "desc",
      priority: 5
    },
    lastUsageDate: { 
      direction: "desc",
      orderByField: "lastSuccessfulPurchaseDate",
      priority: 6
    }
  },
  cardBrandImages: {
    visaSrc: "https://example.com/visa.png",
    mastercardSrc: "https://example.com/mastercard.png",
    amexSrc: "https://example.com/amex.png",
    cupSrc: "https://example.com/cup.png",
    dinersSrc: "https://example.com/diners.png",
    discoverSrc: "https://example.com/discover.png",    
   jcbSrc: "https://example.com/jcb.png",
  },
  isCvcRequired: true,
  cvcComponentConfig: {
    required: true,
    label: "Security Code",
    applyMask: true,
    showMaskToggle: true,
    labelPosition: "left",
    errorMessage: "Please enter a valid security code",
    inputStyles: {
      base: {
        color: "#333",
        fontSize: "16px"
      }
    }
  },
  cardSubmitComponentConfig: {
    submitText: "Pay {amount} {currency}",
    avsRequest: true,
    billingAddressComponents: {
      billingAddressComponent: billingAddress
    },
    // Validate merchant fields alongside SDK billing address
    onCustomValidation: async () => {
      let isValid = true;
      
      const email = document.getElementById('email').value;
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!email || !emailRegex.test(email)) {
        showFieldError('email', 'Please enter a valid email address');
        isValid = false;
      }
      
      const termsAccepted = document.getElementById('terms-checkbox').checked;
      if (!termsAccepted) {
        showFieldError('terms', 'You must accept the terms and conditions');
        isValid = false;
      }
      
      return isValid;
    },
    onPreAuthorisation: async (data) => {
      return { psd2Data: {} };
    },
    onPostTokenisation: (data) => {
      // Store data.gatewayTokenId on your backend. Pass it as gatewayTokenId on a later
      // click-once config so the shopper pays with that token without selecting it.
      saveGatewayTokenId(data.gatewayTokenId);
    },
    onPostAuthorisation: (result) => {
      console.log('Payment completed:', result.merchantTransactionId);
      window.location.href = '/success';
    }
  },
  hideCardBrandLogo: false,
  submitText: "Pay now",
  disableCardSelection: false,
  isRenderLastPurchaseCard: true,
  isRenderLastPayoutCard: true,
  transactionInitiatorType: TransactionInitiatorType.CIT,
  externalTokenId: "ext_token_123456789",
  onOnceCardClick: () => {
    console.log('Card was clicked');
  },
  onPreRenderTokens: (data) => {
    // Filter to only show tokens from USA issuers
    return data.gatewayTokens
      .filter((token) => token.issuerCountryCode === 'USA')
      .map((token) => ({
        id: token.gatewayTokenId,
        isCvcRequired: true
      }));
  },
  onRetrieveTokensFailed: (error) => {
    console.error('Failed to retrieve tokens:', error);
    showErrorMessage('Unable to load saved cards. Please try again.');
  },
  buttonBuilder: (elementIds) => {
    return `
      <div class="custom-token-layout">
        <div id="${elementIds.tokenImageId}" class="token-image"></div>
        <div id="${elementIds.cardNumberId}" class="token-label"></div>
        <div id="${elementIds.cvcComponentId}" class="cvc-input"></div>
        <div id="${elementIds.payNowId}" class="pay-button"></div>
      </div>
    `;
  },
  selectTokenItemBuilder: (elementIds) => {
    return `
      <div class="custom-select-token">
        <div id="${elementIds.tokenImageId}" class="card-brand-image"></div>
        <div id="${elementIds.cardNumberId}" class="card-number"></div>
        <div id="${elementIds.expiryDateId}" class="expiry-date"></div>
      </div>
    `;
  }
};
```

### Payout/disbursement flow

Use the click-once component to send funds to a cardholder's card (common for marketplace payouts, refunds, or prize disbursements):

```typescript
import PxpCheckout, { IntentType } from '@pxp-io/web-components-sdk';

const pxpCheckout = PxpCheckout.initialize({
  session: {
    sessionId: 'your-session-id',
    hmacKey: 'your-hmac-key',
    encryptionKey: 'your-encryption-key'
  },
  environment: 'production',
  transactionData: {
    amount: 150.00,
    currency: 'USD',
    entryType: 'Ecom',
    intent: {
      card: IntentType.Payout  // Critical: Set to Payout for disbursements
    },
    merchantTransactionId: 'payout-' + Date.now(),
    merchantTransactionDate: () => new Date().toISOString()
  },
  kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
  ownerType: 'MerchantGroup',
  ownerId: 'your-owner-id',
  onGetShopper: async () => ({ id: 'shopper-id' })
});

const clickOnceConfig: ClickOnceComponentConfig = {
  limitTokens: 5,
  
  // Enable payout-specific features
  isRenderLastPayoutCard: true,  // Show cards used for previous payouts
  isRenderLastPurchaseCard: false,  // Optionally hide purchase-only cards
  
  // Sort by most recent payout activity
  orderBy: {
    lastUsageDate: { 
      direction: "desc",
      orderByField: "lastSuccessfulPayoutDate",
      priority: 1
    }
  },
  
  submitText: "Send funds",
  
  cardSubmitComponentConfig: {
    submitText: "Withdraw {amount} {currency}",
    onPreAuthorisation: async (data) => {
      return { 
        riskScreeningData: {
          performRiskScreening: true,
          userIp: "192.168.1.100",
          account: {
            id: "recipient_12345",
            creationDateTime: "2024-01-15T10:30:00.000Z"
          },
          fulfillments: [{
            type: "Shipped",
            recipientPerson: {
              phoneNumber: "+1234567890"
            }
          }]
        }
      };
    },
    onPostAuthorisation: (result) => {
      console.log('Payout completed:', result.merchantTransactionId);
      window.location.href = '/payout-confirmation';
    }
  },
  
  onRetrieveTokensFailed: (error) => {
    console.error('Failed to load payout cards:', error);
    showErrorMessage('Unable to load saved cards. Please try again.');
  }
};

const clickOnceComponent = pxpCheckout.create('click-once', clickOnceConfig);
clickOnceComponent.mount('click-once-container');
```

When using `IntentType.Payout`, funds move from your merchant account to the cardholder's card. Ensure you have sufficient balance in your account and use appropriate messaging ("withdraw", "receive", "send to your card"). Note that not all cards support payouts.