@akinon/akifast-secure-form
v2.0.2
Published
Akifast Secure Form Library. This library allows clients to create payment forms within their styles. By using this form clients are also stay in secure field of PCI-DSS
Maintainers
Readme
@akinon/akifast-secure-form
A comprehensive PCI-DSS compliant payment form library for secure card data collection in web applications.
📚 Table of Contents
- Overview
- Features
- Installation
- Two Implementation Approaches
- SecurePaymentForm (Single Iframe)
- SecurePaymentFields (Multiple Iframes)
- Security
- TypeScript Support
- Browser Support
- Migration Guide
- Troubleshooting
Overview
@akinon/akifast-secure-form is a production-ready payment form library designed to help merchants collect card data securely without sensitive information touching their servers. Built with TypeScript and following PCI-DSS security standards, it provides two flexible implementation approaches to suit different use cases.
Why Use This Library?
✅ PCI-DSS Compliant - Sensitive card data never touches your servers
✅ Two Flexible Approaches - Choose between single iframe or individual field iframes
✅ Full TypeScript Support - Comprehensive type definitions with JSDoc
✅ Event-Driven Architecture - Type-safe event subscriptions
✅ Customizable Styling - Full control over form appearance
✅ Automatic Font Sync - Seamlessly matches your site's typography
✅ Input Validation - Real-time validation with custom error messages
✅ BIN Detection - Automatic card brand detection
✅ Production Ready - Used in production by Akinon clients
Features
Core Security Features
- Iframe Isolation: Card data is processed in secure, isolated iframes
- Origin Validation: Strict origin checking prevents XSS attacks
- PostMessage Communication: Secure cross-origin communication
- No Data Leakage: Card details never exposed to parent page JavaScript
- CSP Support: Content Security Policy headers for additional protection
Developer Experience
- TypeScript First: Written in TypeScript with full type definitions
- Event System: Comprehensive event system for all form interactions
- Custom Validation: Define custom validation rules and error messages
- Dynamic Styling: Apply styles programmatically or via configuration
- Font Management: Automatic detection and synchronization of web fonts
- Automatic Resizing: Iframe height adjusts automatically to content
Payment Features
- Multiple Card Types: Visa, Mastercard, American Express, and more
- BIN Lookup: Automatic card brand detection from first 6-8 digits
- Card Tokenization: Secure token generation for payment processing
- Provider Tokenization: Support for payment provider-specific tokenization (e.g., Tap)
- Input Masking: Automatic formatting for card numbers, expiry dates
- CVV Protection: Built-in secret field support with toggle visibility
Installation
NPM
npm install @akinon/akifast-secure-formYarn
yarn add @akinon/akifast-secure-formRequirements
- Node.js >= 18.17.0
- Modern browser with ES6+ support
- iframe-enabled environment
Two Implementation Approaches
This library offers two distinct approaches for integrating secure payment forms:
| Feature | SecurePaymentForm | SecurePaymentFields | |---------|------------------|---------------------| | Iframe Structure | Single iframe containing entire form | Multiple iframes, one per field | | Use Case | Complete form solutions, simple integration | Fine-grained control, custom layouts | | Styling Flexibility | Form-level styling | Individual field-level styling | | DOM Integration | Single container element | Multiple container elements | | Event Granularity | Form-level events | Field-level events | | Best For | Checkout pages, simple forms | Complex UIs, custom designs |
SecurePaymentForm (Single Iframe)
Overview
SecurePaymentForm (also available as AkifastSecureForm) renders the entire payment form inside a single iframe. This approach is ideal for straightforward implementations where you want a complete, self-contained payment form.
Quick Start
import { SecurePaymentForm } from '@akinon/akifast-secure-form';
const paymentForm = new SecurePaymentForm({
iframeHostUrl: 'https://secure-form.example.com',
publicKeyToken: 'pk_test_your_public_key_here',
iframeContainerId: 'payment-form-container',
lang: 'tr', // optional, defaults to 'tr'
formFields: {
groups: [
{
direction: 'column',
fields: [
{
name: 'cardNumber',
type: 'card',
label: 'Kart Numarası',
placeholder: '1234 5678 9012 3456',
style: {
width: '100%',
padding: '12px 16px',
fontSize: '16px',
border: '2px solid #e2e8f0',
borderRadius: '8px'
}
},
{
name: 'cardHolder',
type: 'cardHolder',
label: 'Kart Üzerindeki İsim',
placeholder: 'JOHN DOE'
},
{
name: 'expiryMonth',
type: 'monthText',
label: 'Ay',
placeholder: '12'
},
{
name: 'expiryYear',
type: 'yearText',
label: 'Yıl',
placeholder: '25'
},
{
name: 'cvv',
type: 'cvv',
label: 'CVV',
placeholder: '123',
secret: true,
showSecretToggle: true
}
]
}
]
}
});
// Initialize the form
paymentForm.init();
// Listen for events
paymentForm.onReady(() => {
console.log('Form is ready');
});
paymentForm.onTokenizeSuccess((response) => {
const { merchant_card_token, masked_card_number } = response.data;
console.log('Token:', merchant_card_token);
console.log('Masked Card:', masked_card_number);
// Send token to your backend for payment processing
});
// Trigger tokenization
document.getElementById('pay-button').addEventListener('click', () => {
paymentForm.tokenize();
});Configuration Options
Constructor Parameters
interface IAkifastSecureForm {
iframeHostUrl: string; // Required: HPP server URL
publicKeyToken: string; // Required: Your public API key
iframeContainerId: string; // Required: Container element ID
formFields: IFieldset; // Required: Form structure definition
lang?: string; // Optional: Language code (default: 'tr')
baseStyles?: { // Optional: Global iframe styles
[selector: string]: Partial<CSSStyleDeclaration>;
};
}Field Types
| Type | Description | Validation |
|------|-------------|------------|
| card | Card number input | Luhn algorithm, brand detection |
| cvv | Security code | 3-4 digits |
| cardHolder | Full cardholder name | String validation |
| firstname | First name only | String validation |
| lastname | Last name only | String validation |
| expiryDate | MM/YY combined (masked input) | Date validation |
| expiryDateText | MM/YY combined (text inputs) | Date validation |
| monthText | Expiry month (select/input) | 01-12 |
| yearText | Expiry year (select/input) | Current year + range |
Field Configuration
interface IFlatField {
name: string; // Required: Unique field identifier
type: IFieldType; // Required: Field type from table above
label: string; // Required: Display label
placeholder?: string; // Optional: Placeholder text
style?: Partial<CSSStyleDeclaration>; // Optional: Field-specific CSS
labelStyle?: 'normal' | 'normal-animated' | 'border' | 'border-animated'; // Optional
secret?: boolean; // Optional: Hide input (for CVV)
showSecretToggle?: boolean; // Optional: Show/hide toggle button
maxLength?: number; // Optional: Max character length
minLength?: number; // Optional: Min character length
disabled?: boolean; // Optional: Disable field
disabledValidationErrors?: boolean; // Optional: Suppress error messages
validationMessages?: { // Optional: Custom validation messages
required?: string;
invalid?: string;
min?: string;
max?: string;
};
customProperties?: { // Optional: For expiryDate field
year?: { /* year field config */ };
month?: { /* month field config */ };
};
}Field Validation Rules
Conflicting Field Combinations (will throw error):
- ❌
cardHolder+ (firstnameORlastname) - ❌
expiryDate+expiryDateText - ❌ (
expiryDateORexpiryDateText) + (monthTextORyearText) - ❌ Duplicate field types in the same form
Event System
Lifecycle Events
// Form ready
paymentForm.onReady((data: IReadyEvent) => {
console.log('Form is ready for user interaction');
});Validation Events
// Form becomes valid
paymentForm.onValid((data: IValidEvent) => {
console.log('All fields valid:', data.data);
document.getElementById('pay-button').disabled = false;
});
// Form becomes invalid
paymentForm.onInvalid((data: IInvalidEvent) => {
console.log('Invalid fields:', data.data);
document.getElementById('pay-button').disabled = true;
});
// Field loses focus
paymentForm.onBlur((data: IBlurEvent) => {
console.log(`Field ${data.fieldType} lost focus`);
console.log('Field state:', data.fieldState);
});Card Detection Events
// Valid BIN entered (first 6-8 digits)
paymentForm.onBinEntered((data: IBinEnteredEvent) => {
console.log('Card brand:', data.data.niceType); // e.g., "Visa"
console.log('Card type:', data.data.cardType); // e.g., "visa"
console.log('BIN:', data.data.binNumber);
// Update UI with card brand logo
});
// Invalid BIN entered
paymentForm.onInvalidBinEntered((data: IInvalidBinEnteredEvent) => {
console.log('Invalid card number detected');
});
// BIN lookup failed
paymentForm.onBinNotFound((data: IBinNotFoundEvent) => {
console.log('Could not determine card brand');
});Tokenization Events
// Tokenization success
paymentForm.onTokenizeSuccess((data: ITokenizeSuccessEvent) => {
const {
merchant_card_token, // Use this token for payment
masked_card_number, // e.g., "1234 56** **** 7890"
trace_id // For tracking/debugging
} = data.data;
// Send to your backend
processPayment(merchant_card_token);
});
// Tokenization error
paymentForm.onTokenizeError((data: ITokenizeErrorEvent) => {
console.error('Tokenization failed:', data.message);
showErrorToUser(data.message);
});
// Security violation detected
paymentForm.onSecurityViolation((data: ISecurityViolationEvent) => {
console.warn('Security violation:', data);
// Log security incident
});Payment Provider Tokenization
// Provider-specific tokenization success (e.g., Tap)
paymentForm.onPaymentProviderTokenizationSuccess((data) => {
const {
token,
masked_card_number,
payment_system_code,
tokenization_type
} = data.data;
console.log(`${payment_system_code} token:`, token);
});
// Provider tokenization error
paymentForm.onPaymentProviderTokenizationError((data) => {
console.error('Provider tokenization failed:', data.message);
});API Methods
init(): void
Initializes the secure form iframe and mounts it to the DOM.
paymentForm.init();tokenize(): void
Triggers the card tokenization process. Listen for success/error with event handlers.
document.getElementById('pay-button').onclick = () => {
paymentForm.tokenize();
};paymentProviderTokenization(options): void
Requests tokenization from a payment provider (e.g., Tap for Apple Pay).
paymentForm.paymentProviderTokenization({
paymentSystemCode: 'TAP',
tokenizationType: 'TAP_ENCRYPTION'
});updateFontFamily(): void
Manually syncs the current page's fonts with the iframe.
paymentForm.updateFontFamily();setFontFamily(options): void
Sets a specific font family for the iframe.
paymentForm.setFontFamily({
fontFamily: "'Inter', -apple-system, sans-serif",
stylesheetUrls: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'
],
fontFaceRules: [
`@font-face {
font-family: 'CustomFont';
src: url('https://cdn.example.com/font.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}`
]
});destroy(): void
Cleans up resources, removes event listeners, and destroys the iframe.
window.addEventListener('beforeunload', () => {
paymentForm.destroy();
});Styling
Global Styles (baseStyles)
Apply styles to all elements in the iframe:
const paymentForm = new SecurePaymentForm({
// ... other options
baseStyles: {
body: {
fontFamily: "'Inter', sans-serif",
fontSize: '16px',
color: '#1a202c',
backgroundColor: '#ffffff',
padding: '20px'
},
input: {
borderRadius: '8px',
border: '2px solid #e2e8f0',
padding: '12px 16px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
backgroundColor: '#ffffff'
},
'input:focus': {
borderColor: '#3b82f6',
outline: 'none',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
},
label: {
fontSize: '14px',
fontWeight: '500',
color: '#4a5568',
marginBottom: '6px'
}
}
});Field-Specific Styles
{
name: 'cardNumber',
type: 'card',
label: 'Card Number',
style: {
width: '100%',
padding: '14px 18px',
fontSize: '18px',
fontWeight: '500',
border: '2px solid #cbd5e0',
borderRadius: '12px',
backgroundColor: '#f7fafc',
color: '#2d3748',
transition: 'all 0.2s ease'
}
}Label Styles
Four built-in label animation styles:
// Static label above field
{ labelStyle: 'normal' }
// Label animates above field on focus/fill
{ labelStyle: 'normal-animated' }
// Label inside border (Material Design style)
{ labelStyle: 'border' }
// Label animates to border on focus/fill (Material Design animated)
{ labelStyle: 'border-animated' }Font Management
Automatic Font Synchronization
Fonts are automatically detected and synchronized from:
- Google Fonts
- Adobe Fonts (Typekit)
- Font Awesome
- Custom @font-face rules
- System fonts
<!-- These fonts are automatically synced to the iframe -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
}
body {
font-family: 'Inter', sans-serif;
}
</style>Dynamic Font Changes
Font changes are automatically detected via MutationObserver when:
- CSS files are loaded/unloaded
- Style attributes change
- CSS classes affecting fonts are modified
// Fonts will auto-sync when this happens:
document.body.style.fontFamily = "'Roboto', sans-serif";SecurePaymentFields (Multiple Iframes)
Overview
SecurePaymentFields renders each payment field in its own isolated iframe. This approach provides maximum flexibility and control, allowing you to:
- Position fields anywhere in your layout
- Apply individual styling to each field
- Handle events at the field level
- Create completely custom payment UIs
Quick Start
import { SecurePaymentFields } from '@akinon/akifast-secure-form';
// Initialize the payment form instance
const paymentForm = SecurePaymentFields({
iframeHostUrl: 'https://secure-form.example.com',
publicKeyToken: 'pk_test_your_public_key_here',
locale: 'tr' // optional
});
// Create individual fields
const cardNumber = paymentForm.createElement('cardNumber', {
placeholder: '1234 5678 9012 3456',
style: {
base: {
fontSize: '16px',
color: '#2d3748',
'::placeholder': {
color: '#a0aec0'
}
},
invalid: {
color: '#e53e3e',
borderColor: '#fc8181'
},
complete: {
borderColor: '#48bb78'
}
},
classes: {
base: 'secure-field',
focus: 'secure-field--focus',
invalid: 'secure-field--invalid',
complete: 'secure-field--complete'
}
});
const cardExpiry = paymentForm.createElement('cardExpiry', {
placeholder: 'MM / YY',
useExpirySelect: false // true for dropdown, false for input
});
const cardCvv = paymentForm.createElement('cardCvv', {
placeholder: '123'
});
const cardHolder = paymentForm.createElement('cardHolder', {
placeholder: 'JOHN DOE'
});
// Mount fields to DOM elements
cardNumber.mount('#card-number-element');
cardExpiry.mount('#card-expiry-element');
cardCvv.mount('#card-cvv-element');
cardHolder.mount('#card-holder-element');
// Field-level events
cardNumber.on('change', (state) => {
console.log('Card number changed:', state);
if (state.brand) {
updateCardBrandIcon(state.brand);
}
});
// Form-level events (fires for any field)
paymentForm.on('change', (fieldType, state) => {
console.log(`${fieldType} changed:`, state);
});
// Tokenization
document.getElementById('pay-button').addEventListener('click', async () => {
try {
const result = await paymentForm.tokenize();
console.log('Token:', result);
// Send to backend
} catch (error) {
console.error('Tokenization failed:', error);
}
});HTML Structure
<form id="payment-form">
<div class="form-row">
<label for="card-number-element">Card Number</label>
<div id="card-number-element" class="field-container"></div>
</div>
<div class="form-row">
<label for="card-holder-element">Cardholder Name</label>
<div id="card-holder-element" class="field-container"></div>
</div>
<div class="form-row">
<div class="form-col">
<label for="card-expiry-element">Expiry</label>
<div id="card-expiry-element" class="field-container"></div>
</div>
<div class="form-col">
<label for="card-cvv-element">CVV</label>
<div id="card-cvv-element" class="field-container"></div>
</div>
</div>
<button type="submit" id="pay-button">Pay Now</button>
</form>Configuration
PaymentForm Constructor
interface PaymentFieldConfig {
iframeHostUrl: string; // Required: HPP server URL
publicKeyToken: string; // Required: Your public API key
locale?: string; // Optional: Language code
}Field Types
type FieldType = 'cardNumber' | 'cardExpiry' | 'cardCvv' | 'cardHolder';Field Options
interface FieldOptions {
placeholder?: string;
useExpirySelect?: boolean; // Only for cardExpiry - use dropdowns instead of input
// Styling via CSS properties (applied to iframe input)
style?: {
base?: CSSProperties; // Default state
complete?: CSSProperties; // When field is complete
empty?: CSSProperties; // When field is empty
invalid?: CSSProperties; // When field is invalid
focus?: CSSProperties; // When field is focused
};
// Styling via CSS classes (applied to container element)
classes?: {
base?: string;
complete?: string;
empty?: string;
invalid?: string;
focus?: string;
};
}Styling Approaches
Approach 1: CSS Classes (Recommended)
Apply classes to the container element based on field state:
const cardNumber = paymentForm.createElement('cardNumber', {
classes: {
base: 'secure-field',
focus: 'is-focused',
invalid: 'has-error',
complete: 'is-complete',
empty: 'is-empty'
}
});.secure-field {
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
transition: all 0.2s ease;
}
.secure-field.is-focused {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.secure-field.has-error {
border-color: #ef4444;
}
.secure-field.is-complete {
border-color: #10b981;
}
.secure-field iframe {
border: none;
width: 100%;
height: 40px;
}Approach 2: Direct Styles
Apply styles directly to the input element inside the iframe:
const cardNumber = paymentForm.createElement('cardNumber', {
style: {
base: {
fontSize: '16px',
fontFamily: "'Inter', sans-serif",
color: '#2d3748',
'::placeholder': {
color: '#a0aec0'
}
},
focus: {
color: '#1a202c'
},
invalid: {
color: '#e53e3e'
},
complete: {
color: '#10b981'
}
}
});Approach 3: Hybrid (Both Classes and Styles)
Combine both approaches for maximum flexibility:
const cardNumber = paymentForm.createElement('cardNumber', {
classes: {
base: 'field-wrapper',
focus: 'field-wrapper--focus'
},
style: {
base: {
fontSize: '16px',
padding: '12px'
},
invalid: {
color: '#e53e3e'
}
}
});Field Events
Each field supports individual event listeners:
// ready: Field iframe is loaded and ready
cardNumber.on('ready', (state) => {
console.log('Card number field is ready');
});
// focus: Field received focus
cardNumber.on('focus', (state) => {
console.log('Card number focused');
});
// blur: Field lost focus
cardNumber.on('blur', (state) => {
console.log('Card number blurred');
if (!state.isValid && !state.isEmpty) {
showFieldError('Please enter a valid card number');
}
});
// change: Field value changed
cardNumber.on('change', (state) => {
console.log('Card number state:', state);
// state properties:
// - isValid: boolean
// - isEmpty: boolean
// - isComplete: boolean
// - isFocused: boolean
// - error?: string
// - brand?: string (only for cardNumber)
// - bin?: string (only for cardNumber, first 8 digits)
});
// validation: Field validation state changed
cardNumber.on('validation', (state) => {
if (!state.isValid && !state.isEmpty) {
console.error('Invalid card number');
}
});
// binEntered: Valid BIN detected (cardNumber only)
cardNumber.on('binEntered', (state) => {
console.log('BIN detected:', state.bin);
console.log('Card brand:', state.brand);
updateCardBrandIcon(state.brand);
});
// binCleared: BIN cleared (cardNumber only)
cardNumber.on('binCleared', (state) => {
console.log('BIN cleared');
removeCardBrandIcon();
});Form-Level Events
Listen to events from all fields:
// Fires when any field changes
paymentForm.on('change', (fieldType, state) => {
console.log(`${fieldType} changed:`, state);
updateFormValidation();
});
// Fires when any field receives focus
paymentForm.on('focus', (fieldType, state) => {
console.log(`${fieldType} focused`);
});
// Fires when any field loses focus
paymentForm.on('blur', (fieldType, state) => {
console.log(`${fieldType} blurred`);
});
// Fires when any field is ready
paymentForm.on('ready', (fieldType, state) => {
console.log(`${fieldType} is ready`);
});
// Fires when BIN is entered in card number field
paymentForm.on('binEntered', (fieldType, state) => {
if (fieldType === 'cardNumber') {
console.log('Card brand:', state.brand);
}
});Field Methods
mount(selector: string | HTMLElement): void
Mounts the field iframe to a DOM element.
cardNumber.mount('#card-number-element');
// or
cardNumber.mount(document.getElementById('card-number-element'));unmount(): void
Removes the field iframe from the DOM.
cardNumber.unmount();clear(): void
Clears the field value.
cardNumber.clear();focus(): void
Programmatically focuses the field.
cardNumber.focus();blur(): void
Programmatically blurs the field.
cardNumber.blur();update(options: FieldOptions): void
Updates field options (styles, placeholder, etc.).
cardNumber.update({
placeholder: 'Enter your card number',
style: {
base: {
fontSize: '18px'
}
}
});getState(): FieldState
Gets the current field state.
const state = cardNumber.getState();
console.log('Is valid:', state.isValid);
console.log('Is complete:', state.isComplete);
console.log('Card brand:', state.brand);Form Methods
createElement(fieldType: FieldType, options?: FieldOptions): SecureField
Creates a new secure field.
const cardNumber = paymentForm.createElement('cardNumber', {
placeholder: '1234 5678 9012 3456'
});getField(fieldType: FieldType): SecureField | undefined
Gets a field by type.
const cardNumber = paymentForm.getField('cardNumber');removeField(fieldType: FieldType): void
Removes a field and unmounts it.
paymentForm.removeField('cardNumber');tokenize(): Promise<TokenResult>
Creates a payment token from all field values.
try {
const result = await paymentForm.tokenize();
console.log('Token:', result.token);
// Send token to backend
await processPayment(result.token);
} catch (error) {
console.error('Tokenization failed:', error.message);
showErrorToUser(error.message);
}paymentProviderTokenization(options): Promise<TokenResult>
Creates a token using a payment provider.
try {
const result = await paymentForm.paymentProviderTokenization({
paymentSystemCode: 'TAP',
tokenizationType: 'TAP_ENCRYPTION'
});
console.log('Provider token:', result.token);
} catch (error) {
console.error('Provider tokenization failed:', error);
}getStates(): Map<FieldType, FieldState>
Gets all field states.
const states = paymentForm.getStates();
states.forEach((state, fieldType) => {
console.log(`${fieldType}:`, state.isValid);
});clear(): void
Clears all fields.
paymentForm.clear();destroy(): void
Destroys all fields and cleans up resources.
paymentForm.destroy();Complete Example
import { SecurePaymentFields } from '@akinon/akifast-secure-form';
// Initialize
const paymentForm = SecurePaymentFields({
iframeHostUrl: 'https://secure-form.example.com',
publicKeyToken: 'pk_test_abc123',
locale: 'tr'
});
// Create fields
const fields = {
cardNumber: paymentForm.createElement('cardNumber', {
placeholder: 'Card Number',
classes: {
base: 'field',
focus: 'field--focus',
invalid: 'field--error',
complete: 'field--success'
}
}),
cardExpiry: paymentForm.createElement('cardExpiry', {
placeholder: 'MM / YY',
classes: { base: 'field' }
}),
cardCvv: paymentForm.createElement('cardCvv', {
placeholder: 'CVV',
classes: { base: 'field' }
}),
cardHolder: paymentForm.createElement('cardHolder', {
placeholder: 'CARDHOLDER NAME',
classes: { base: 'field' }
})
};
// Mount fields
fields.cardNumber.mount('#card-number');
fields.cardExpiry.mount('#card-expiry');
fields.cardCvv.mount('#card-cvv');
fields.cardHolder.mount('#card-holder');
// Field events
fields.cardNumber.on('binEntered', (state) => {
document.getElementById('card-brand').textContent = state.brand;
});
// Form validation
const payButton = document.getElementById('pay-button');
paymentForm.on('change', () => {
const states = paymentForm.getStates();
const allValid = Array.from(states.values()).every(s => s.isValid);
payButton.disabled = !allValid;
});
// Handle payment
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
payButton.disabled = true;
payButton.textContent = 'Processing...';
try {
const result = await paymentForm.tokenize();
// Send to backend
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: result.token })
});
if (response.ok) {
window.location.href = '/success';
} else {
throw new Error('Payment failed');
}
} catch (error) {
alert('Payment failed: ' + error.message);
payButton.disabled = false;
payButton.textContent = 'Pay Now';
}
});
// Cleanup
window.addEventListener('beforeunload', () => {
paymentForm.destroy();
});Security
PCI-DSS Compliance
This library helps maintain PCI-DSS compliance by:
- Iframe Isolation: Sensitive data is processed in iframes hosted on your secure domain
- No Direct Access: Card data never touches your client-side JavaScript
- Tokenization: Card data is converted to tokens before leaving the iframe
- Origin Validation: Strict cross-origin checks prevent data leakage
Security Best Practices
1. Use HTTPS Everywhere
// ✅ GOOD
iframeHostUrl: 'https://secure-form.example.com'
// ❌ BAD
iframeHostUrl: 'http://secure-form.example.com'2. Implement CSP Headers
Content-Security-Policy:
default-src 'self';
frame-src https://secure-form.example.com;
script-src 'self';
style-src 'self' 'unsafe-inline';3. Validate Tokens Server-Side
// Never trust client-side validation
// Always verify tokens on your backend
app.post('/process-payment', async (req, res) => {
const { token } = req.body;
// Verify token with payment provider
const result = await paymentProvider.verifyToken(token);
if (!result.valid) {
return res.status(400).json({ error: 'Invalid token' });
}
// Process payment
});4. Monitor Security Events
paymentForm.onSecurityViolation((data) => {
// Log to security monitoring system
logSecurityIncident({
type: 'form_violation',
timestamp: new Date(),
data: data
});
});TypeScript Support
Full TypeScript support with comprehensive type definitions.
Importing Types
// SecurePaymentForm types
import {
SecurePaymentForm,
IAkifastSecureForm,
IFieldset,
IGroup,
IFlatField,
IFieldType,
IReadyEvent,
IBlurEvent,
IValidEvent,
IInvalidEvent,
IBinEnteredEvent,
ITokenizeSuccessEvent,
ITokenizeErrorEvent,
IFontInfo
} from '@akinon/akifast-secure-form';
// SecurePaymentFields types
import {
SecurePaymentFields,
PaymentForm,
SecureField,
FieldType,
FieldOptions,
FieldState,
PaymentFieldConfig,
TokenResult,
EventType,
EventHandler
} from '@akinon/akifast-secure-form';
// Utility types (validators, formatters)
import {
validateCardNumber,
validateCVV,
formatCardNumber,
formatExpiry
} from '@akinon/akifast-secure-form';Type-Safe Configuration
const config: IAkifastSecureForm = {
iframeHostUrl: 'https://secure.example.com',
publicKeyToken: 'pk_test_123',
iframeContainerId: 'form-container',
formFields: {
groups: [
{
direction: 'column',
fields: [
{
name: 'cardNumber',
type: 'card',
label: 'Card Number',
placeholder: '1234 5678 9012 3456'
}
]
}
]
}
};
const form = new SecurePaymentForm(config);Type-Safe Event Handlers
form.onTokenizeSuccess((response: ITokenizeSuccessEvent) => {
// response.data is fully typed
const token: string = response.data.merchant_card_token;
const masked: string = response.data.masked_card_number;
});
form.onBinEntered((response: IBinEnteredEvent) => {
const cardType: string | null = response.data.cardType;
const niceType: string | null = response.data.niceType;
});Browser Support
Minimum Requirements
- Chrome: 80+
- Firefox: 75+
- Safari: 13+
- Edge: 80+
- iOS Safari: 13+
- Chrome Mobile: 80+
Required Browser Features
- ✅ ES6+ JavaScript support
- ✅
postMessageAPI - ✅
MutationObserverAPI - ✅
Promisesupport - ✅ CSS Custom Properties
- ✅ iframe support
Polyfills
No polyfills required for modern browsers. For older browsers, include:
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=Promise,MutationObserver"></script>Migration Guide
From Custom Solution to SecurePaymentForm
Before (Custom Form)
<form>
<input type="text" name="cardNumber" />
<input type="text" name="cvv" />
<button>Pay</button>
</form>After (SecurePaymentForm)
import { SecurePaymentForm } from '@akinon/akifast-secure-form';
const form = new SecurePaymentForm({
iframeHostUrl: 'https://secure.example.com',
publicKeyToken: 'pk_test_xxx',
iframeContainerId: 'payment-form',
formFields: {
groups: [{
direction: 'column',
fields: [
{ name: 'card', type: 'card', label: 'Card Number' },
{ name: 'cvv', type: 'cvv', label: 'CVV' }
]
}]
}
});
form.init();From SecurePaymentForm to SecurePaymentFields
If you need more control, migrate to individual field iframes:
Before (Single Iframe)
const form = new SecurePaymentForm({
iframeHostUrl: 'https://secure.example.com',
publicKeyToken: 'pk_test_xxx',
iframeContainerId: 'form',
formFields: { /* ... */ }
});After (Multiple Iframes)
const form = SecurePaymentFields({
iframeHostUrl: 'https://secure.example.com',
publicKeyToken: 'pk_test_xxx'
});
const cardNumber = form.createElement('cardNumber');
cardNumber.mount('#card-number');
const cardCvv = form.createElement('cardCvv');
cardCvv.mount('#card-cvv');Troubleshooting
Common Issues
Form Not Loading
Symptoms: Iframe doesn't appear or shows blank content
Solutions:
- Verify
iframeHostUrlis correct and accessible - Check browser console for CORS errors
- Ensure
publicKeyTokenis valid - Verify container element exists:
document.getElementById(iframeContainerId)
// Debug
console.log('Container exists:', !!document.getElementById('form-container'));
console.log('HPP URL accessible:', 'https://secure.example.com');Events Not Firing
Symptoms: Event handlers are not being called
Solutions:
- Call
init()before setting up event listeners (SecurePaymentForm) - Call
mount()before adding event listeners (SecurePaymentFields) - Verify origin matches iframe URL
- Check browser console for security errors
// ✅ CORRECT ORDER
form.init();
form.onReady(() => console.log('Ready'));
// ❌ WRONG ORDER
form.onReady(() => console.log('Ready'));
form.init();Styling Not Applied
Symptoms: Custom styles don't appear in iframe
Solutions:
- Use camelCase for CSS properties:
fontSizenotfont-size - Ensure styles don't violate iframe CSP
- Check browser console for style injection errors
- Verify selectors in
baseStylestarget correct elements
// ✅ CORRECT
style: {
fontSize: '16px',
borderRadius: '8px'
}
// ❌ INCORRECT
style: {
'font-size': '16px', // Use camelCase
border-radius: '8px' // Use camelCase
}Font Synchronization Issues
Symptoms: Iframe uses different fonts than parent page
Solutions:
- Ensure fonts are loaded before calling
init() - Verify font URLs are accessible from iframe domain
- Call
updateFontFamily()after dynamic font changes - Use
setFontFamily()for manual font control
// Wait for fonts to load
document.fonts.ready.then(() => {
form.init();
});
// Or manually set fonts
form.setFontFamily({
fontFamily: "'Inter', sans-serif",
stylesheetUrls: ['https://fonts.googleapis.com/css2?family=Inter']
});Tokenization Fails
Symptoms: onTokenizeError fires instead of onTokenizeSuccess
Solutions:
- Verify all required fields are filled
- Check validation state before calling
tokenize() - Ensure network connection to HPP server
- Verify
publicKeyTokenis valid and not expired
// Check form validity first
form.onValid(() => {
document.getElementById('pay-button').disabled = false;
});
form.onInvalid(() => {
document.getElementById('pay-button').disabled = true;
});Debugging Tips
Enable Verbose Logging
// Check iframe creation
form.onReady(() => {
console.log('✅ Form is ready');
});
// Monitor all events
form.onBlur((data) => {
console.log('Field blurred:', data);
});
form.onValid((data) => {
console.log('Form valid:', data);
});
form.onInvalid((data) => {
console.log('Form invalid:', data);
});Inspect Field States (SecurePaymentFields)
const states = paymentForm.getStates();
states.forEach((state, fieldType) => {
console.log(`${fieldType}:`, {
isValid: state.isValid,
isEmpty: state.isEmpty,
isComplete: state.isComplete,
error: state.error
});
});Network Debugging
Check browser Network tab for:
- Iframe HTML loading correctly
- postMessage communication working
- No CORS errors
- API calls to tokenization endpoint
License
ISC License - Copyright (c) Akinon
