Click to Pay — c2p-extension component
The c2p-extension component renders only the Click to Pay experience (card list, OTP, consent) — no card input fields. Use it when you already have your own card form and want to add Click to Pay alongside it.
If you don't have your own card form and want everything in one iframe, use card instead. For shared concepts (enablement, states, accessibility), see the Click to Pay overview.
Prerequisites
Include the Components script, your own card form, and a container for the extension:
<script src="https://ipgtest.monri.com/dist/components.js"></script>
<!-- Your own card form -->
<form id="your-card-form">
<input name="pan" placeholder="Card number" />
<input name="expiry" placeholder="MM/YY" />
<input name="cvv" placeholder="CVV" />
</form>
<!-- c2p-extension mounts here -->
<div id="card-element"></div>
Integration
// Initialize Monri
const monri = Monri('your_authenticity_token', {
locale: 'en'
});
// Create a components instance
const components = monri.components({
clientSecret: 'your_client_secret'
});
// Create the c2p-extension component
const card = components.create("c2p-extension", {
clientSecret: 'your_client_secret',
locale: 'en',
environment: 'test', // 'test' or 'prod'
// Click to Pay configuration (optional — auto-detected from the merchant's backend settings)
clickToPay: {
chPhone: '+385991234567', // Customer phone (optional)
chEmail: 'customer@example.com' // Customer email (optional)
},
// Light theme styling (optional)
style: {
customComponents: {
backgroundColor: '#ffffff',
textColor: '#333333',
primaryColor: '#8132A7',
borderColor: '#e0e0e0',
surfaceColor: 'rgba(0, 0, 0, .02)',
radioButtonBackgroundColor: 'rgba(0, 0, 0, .02)',
radioButtonSelectedBackgroundColor: '#FCF5FF',
secondaryTextColor: '#575757',
successColor: '#4caf50',
errorColor: '#f44336',
errorBackgroundColor: 'rgba(244, 67, 54, 0.1)',
successBorderColor: '2px solid #4caf50',
errorBorderColor: '2px solid #f44336'
}
},
// Dark theme styling (optional)
darkThemeStyle: {
customComponents: {
backgroundColor: 'rgba(0, 0, 0, 1)',
textColor: '#ffffff',
primaryColor: '#FF66FF',
borderColor: '#444444',
surfaceColor: 'rgba(255, 255, 255, .05)',
radioButtonBackgroundColor: 'rgba(255, 255, 255, .05)',
radioButtonSelectedBackgroundColor: '#1A001F',
secondaryTextColor: '#FFFFFF',
successColor: '#81c784',
errorColor: '#e57373',
errorBackgroundColor: 'rgba(229, 115, 115, 0.1)',
successBorderColor: '2px solid #81c784',
errorBorderColor: '2px solid #e57373'
}
}
});
// Mount the component
card.mount("card-element");
Component options
| Property | Type | Required | Description |
|---|---|---|---|
clientSecret |
string |
Yes | Your client secret |
locale |
string |
No | Component locale (e.g. 'en', 'hr', 'de') |
environment |
'test' | 'prod' |
Yes | Environment ('test' for sandbox, 'prod' for production) |
clickToPay |
object |
No | Click to Pay configuration (see below). Optional — auto-detected from the merchant's backend settings. If omitted, Click to Pay still activates when mastercard_c2p_active is true for the merchant. Pass it to provide customer contact info or accessibility preferences. |
style |
object |
No | Light theme styling |
darkThemeStyle |
object |
No | Dark theme styling |
Click to Pay configuration
The entire clickToPay object is optional. When omitted, availability is auto-detected from the merchant's backend configuration (mastercard_c2p_active flag in the transaction hash). All fields within the object are also optional.
Note: identity lookup is triggered as soon as either
chEmailorchPhoneis provided — both are not required. If the first identity finds no saved cards, the lookup automatically retries when the second identity is provided.
| Property | Type | Required | Description |
|---|---|---|---|
chPhone |
string |
No | Customer phone number (E.164 format) — used for identity lookup |
chEmail |
string |
No | Customer email address — used for identity lookup |
isHighContrast |
'true' | 'false' |
No | High-contrast mode for accessibility |
dyslexicFont |
'true' | 'false' |
No | Dyslexia-friendly font |
fontSize |
'normal' | 'large' | 'larger' |
No | Font-size scaling |
Minimal integration:
const card = components.create("c2p-extension", {
clientSecret: 'your_client_secret',
environment: 'test'
});
Styling properties
Both style.customComponents and darkThemeStyle.customComponents accept:
| Property | Type | Description |
|---|---|---|
backgroundColor |
string |
Main background color |
textColor |
string |
Primary text color |
primaryColor |
string |
Brand/accent color |
borderColor |
string |
Border color for inputs |
surfaceColor |
string |
Surface/card background |
radioButtonBackgroundColor |
string |
Radio button background |
radioButtonSelectedBackgroundColor |
string |
Selected radio button background |
secondaryTextColor |
string |
Secondary text color |
successColor |
string |
Success state color |
errorColor |
string |
Error state color |
errorBackgroundColor |
string |
Error background color |
successBorderColor |
string |
Success border style (e.g. '2px solid #4caf50') |
errorBorderColor |
string |
Error border style (e.g. '2px solid #f44336') |
Layout & sizing
Viewport breakpoints
The component detects the parent window's viewport width (not the iframe's own width):
| Viewport | Parent width | CSS class on iframe body |
|---|---|---|
| Mobile | <= 575px | viewport-mobile |
| Tablet | 576px – 760px | viewport-tablet |
| Desktop | > 760px | viewport-desktop |
Width
The c2p-extension iframe uses width: 1px; min-width: 100% — it fills its parent container. Size the parent <div> to control the component's width. All states render at 100% width on all viewports; plan your own card-form layout accordingly.
Height
The iframe height adjusts dynamically per state. Because c2p-extension has no card input fields, heights are smaller than the card component. Base heights (normal font size):
| State | Mobile (<=575px) | Desktop (>575px) | Notes |
|---|---|---|---|
empty |
180px | 180px | Click to Pay hidden, minimal height |
loading |
160px | 160px | Loading spinner only |
consent |
~130px+ | ~110px+ | Consent text + checkbox (dynamic) |
card-list |
~90px/card | ~90px/card | Per-card height, paginated at 3 |
otp-input |
300px | 300px | +125–135px if validation channels shown |
new-card |
~90px+ | ~70px+ | New-card consent only (dynamic) |
not-your-card |
~200px+ | ~200px+ | Identity switch form (dynamic) |
Heights scale proportionally with font size (large ~1.14×, larger ~1.29×). Error messages add ~54px+ when visible.
Layout recommendations
Because c2p-extension renders alongside your own card form, use the state_change event to coordinate layout:
empty/new-card/consent— your card form is visible; stack the component below or beside it. Bothconsentandnew-cardappear only after the shopper enters a valid PAN (viaupdateActiveBrand).card-list/otp-input/not-your-card— your card form should be hidden; the component handles the full flow.loading— optionally show your card form while Click to Pay initializes.
Events
State change
Fires when the Click to Pay UI transitions between states. Use it to show or hide your own card form.
card.addChangeListener('state_change', (event) => {
const state = event.data.state;
const showOwnForm = (state === 'empty' || state === 'new-card' || state === 'consent');
document.getElementById('your-card-form').hidden = !showOwnForm;
});
| State | Description | Show your own card form? |
|---|---|---|
'loading' |
The Click to Pay SDK is initializing | No |
'empty' |
Initial state, or SDK failed / no identity found. Consent appears once the shopper enters a valid PAN (via updateActiveBrand) |
Yes |
'consent' |
New-user consent screen — appears when a valid card brand is detected from PAN input; disappears if the PAN is cleared | Yes (alongside Click to Pay) |
'card-list' |
Returning shopper sees saved cards | No |
'otp-input' |
OTP verification screen | No |
'new-card' |
Shopper is adding a new card — appears when a valid card brand is detected from PAN input; disappears if the PAN is cleared | Yes (alongside Click to Pay) |
'not-your-card' |
Shopper is switching identity | No |
Installments
Installments ownership is split between your form and the c2p-extension iframe depending on the active state; each path is gated by a different backend flag.
New-card / consent state (shopper is typing a PAN in your own form)
Your form owns the installments UI here — c2p-extension has no PAN input. Gated by merchant.installment_enabled (top-level merchant setting).
- When the PAN reaches 11+ digits, your form calls
/pan-info - Use the response's
installmentsarray to render your own installments dropdown - Required: call
card.setPanInfo(panInfoResponse)to push the pan-info into the iframe. The iframe's checkout manager usesthree_ds_infofrom this response (acquirerBIN,acquirerMerchantId,merchantCategoryCode,merchantCountryCode) when calling the Mastercard SDK. Without this call, new-card checkout fails with'Acquirer data not available for this card' - When the shopper confirms, include the selected installment value in
NewCreditCardPayment.cardDetails.installments; it flows through to v2/transaction
Card-list state (returning shopper with saved cards)
The iframe handles this path autonomously. Gated by mastercard_c2p_settings.allow_installments — a separate flag inside the Click to Pay merchant config, independent of merchant.installment_enabled.
- Pass
showInstallmentsSelection: truein the component options (controls the card-list BIN lookup inside the iframe) mastercard_c2p_settings.allow_installments: truein the transaction hash/pan-inforeturns more than one installment option for the selected card's BIN
const card = components.create("c2p-extension", {
clientSecret: 'your_client_secret',
environment: 'test',
showInstallmentsSelection: true
});
When these conditions are met, the iframe fetches /pan-info for all saved cards in parallel during the loading state, shows the dropdown immediately on card selection, and includes the selected installment value in the checkout_data event payload (event.data.installments). Your form applies it before calling v2/transaction. Cards remain selectable regardless of BIN data availability; if there are no installment options, the payment proceeds as a single charge.
Checkout data
Fires when a Click to Pay payment completes. Contains the transaction result — forward it to your backend for processing.
card.addChangeListener('checkout_data', (event) => {
console.log('Payment result:', event.data);
});
// checkout_data event payload
type CheckoutDataEvent = {
data: {
response: {
result: {
transaction: {
id: number;
acquirer: string;
order_number: string;
amount: number;
currency: string;
approval_code: string;
response_code: string;
response_message: string;
cc_type: string;
status: string; // 'approved' | 'declined'
payment_method: string; // 'mastercard-c2p'
};
} | null;
error?: string;
};
};
};
Not your card
Fires when the shopper wants to switch accounts or enter a card manually.
card.addChangeListener('not_your_card', (event) => {
const { inputType, inputValue } = event.data;
// inputType: 'email' | 'phone' | 'manual'
// inputValue: string | null
});
Payment progress
Fires during checkout to report progress.
card.addChangeListener('payment_progress', (event) => {
console.log('Progress:', event.data.state, event.data.message);
});
type PaymentProgressPayload = {
data: {
state:
| 'CHECKOUT_IN_PROGRESS'
| 'USER_CANCELLED'
| 'USER_CHANGING_CARD'
| 'AWAITING_CARD_SELECTION'
| 'PROCESSING_TRANSACTION'
| 'COMPLETE';
message?: string;
};
};
Payment processing
Because c2p-extension has no card input fields, payment depends on the path the shopper took.
Click to Pay path (shopper selected a saved card or completed Click to Pay checkout)
Handled automatically through the Mastercard SDK. Listen for checkout_data — no additional API call is needed from your side.
Traditional path (shopper fills your own card form)
When the state is 'empty', 'new-card', or 'consent', the shopper may fill your own card form instead. Submit the payment through submitPayment:
card.submitPayment({
clientSecret: 'your_client_secret',
address: "123 Main Street",
city: "New York",
email: "customer@example.com",
fullName: "John Doe",
phone: "+1-555-123-4567",
country: "US",
zip: "10001",
orderInfo: "Order #12345"
}).then((response) => {
console.log('Payment response:', response);
});
Example checkout response
{
"response": {
"result": {
"transaction": {
"id": 1283,
"acquirer": "Erste HR",
"order_number": "random1760453433569",
"amount": 100,
"currency": "EUR",
"approval_code": "165101",
"response_code": "0000",
"response_message": "Approved or completed successfully",
"cc_type": "master",
"status": "approved",
"payment_method": "mastercard-c2p"
}
}
}
}
The checkoutActionCode in the Mastercard response indicates the outcome:
| Action code | Meaning |
|---|---|
COMPLETE |
Checkout completed successfully |
CANCEL |
Shopper cancelled the checkout |
CHANGE_CARD |
Shopper wants to select a different card |
ADD_CARD |
Shopper wants to add a new card |
Card-brand sync (required)
Because c2p-extension has no card input fields, it cannot detect what card brand the shopper is entering in your own form. You must call updateActiveBrand() whenever the card number changes so the consent screen can appear at the right time.
Per Mastercard requirement 8.4.0, the consent UI (both the consent and new-card states) must appear dynamically while the shopper enters their PAN — not immediately on page load. The component relies on updateActiveBrand() to know when a valid card brand is detected or when the PAN is cleared.
What to do
- Listen for changes on your card-number input
- Detect the card brand from the entered digits (e.g.
5xxx= Mastercard,4xxx= Visa) - Call
card.updateActiveBrand(detectedBrand)when a brand is recognized - Call
card.updateActiveBrand('')when the PAN is cleared or the brand is no longer recognized
const panInput = document.getElementById('pan');
panInput.addEventListener('input', () => {
const value = panInput.value.replace(/\s/g, '');
const brand = detectCardBrand(value); // your brand-detection logic
card.updateActiveBrand(brand || '');
});
function detectCardBrand(pan) {
if (/^5[1-5]/.test(pan) || /^2[2-7]/.test(pan)) return 'mastercard';
if (/^4/.test(pan)) return 'visa';
return '';
}
How it affects the state
| PAN input state | updateActiveBrand() call |
State transition |
|---|---|---|
| Empty / no brand detected | card.updateActiveBrand('') |
Stays in or transitions to 'empty' |
| Valid brand detected (new user) | card.updateActiveBrand('mastercard') |
'empty' → 'consent' |
| Valid brand detected (after "Add card") | card.updateActiveBrand('mastercard') |
'empty' → 'new-card' |
| PAN cleared after brand was detected | card.updateActiveBrand('') |
'consent' or 'new-card' → 'empty' |
Important: if you do not call
updateActiveBrand(), neither the consent nor the new-card screen appears, and the shopper is not offered Click to Pay for new-card enrolment. This only affects the consent/new-card flow — returning shoppers with saved cards (card-list, OTP) are unaffected.
Runtime methods
card.setLanguage('hr'); // update language
card.toggleTheme(true); // true = dark, false = light
card.toggleDyslexicFont(true); // dyslexic font
card.toggleFontSize('large'); // 'normal' | 'large' | 'larger'
card.clientPhoneUpdate('+385991234567'); // update contact — re-runs identity lookup
card.clientEmailUpdate('new@example.com');
card.updateActiveBrand('mastercard'); // brand detected
card.updateActiveBrand(''); // PAN cleared or no brand
Accessibility
Click to Pay includes comprehensive accessibility support:
- High-contrast mode — set via
isHighContrastortoggleTheme()at runtime - Dyslexic font — OpenDyslexic font family, via
dyslexicFontortoggleDyslexicFont() - Font-size scaling —
normal,large,larger, viafontSizeortoggleFontSize() - Theme support — light and dark variants with automatic contrast adjustments
const card = components.create("c2p-extension", {
clientSecret: 'your_client_secret',
environment: 'test',
clickToPay: {
isHighContrast: localStorage.getItem('highContrast') ?? 'false',
dyslexicFont: localStorage.getItem('dyslexicFont') ?? 'false',
fontSize: localStorage.getItem('fontSizeState') ?? 'normal'
}
});