# Card Linking - Integration Examples

Complete integration examples for implementing card linking using ABA PayWay's Card on File feature.

⚠️ **IMPORTANT**: `returnUrl` and `continueAddCardSuccessUrl` MUST be base64 encoded. Plain URLs will be rejected.

## Base64 Encoding URLs

```typescript
// Helper function to encode URLs to base64
function encodeUrlToBase64(url: string): string {
  return Buffer.from(url).toString('base64');
}

// Example usage
const returnUrl = 'https://yourapp.com/api/card-linked';
const encodedReturnUrl = encodeUrlToBase64(returnUrl);
// Result: aHR0cHM6Ly95b3VyYXBwLmNvbS9hcGkvY2FyZC1saW5rZWQ=

const continueUrl = 'https://yourapp.com/card-success';
const encodedContinueUrl = encodeUrlToBase64(continueUrl);
// Result: aHR0cHM6Ly95b3VyYXBwLmNvbS9jYXJkLXN1Y2Nlc3M=
```

## 1. Express.js Full Example

Complete Express.js server with card linking and pushback handling.

```typescript
import express from 'express';
import { PayWayClient } from 'aba-payway-sdk';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const payway = new PayWayClient({
  merchantId: process.env.PAYWAY_MERCHANT_ID,
  apiKey: process.env.PAYWAY_API_KEY,
  sandbox: true
});

// Helper function to encode URLs to base64
function encodeUrlToBase64(url: string): string {
  return Buffer.from(url).toString('base64');
}

// Generate form parameters for client-side form
app.post('/api/generate-card-form-params', async (req, res) => {
  try {
    const { userId, firstname, lastname, email, phone } = req.body;

    // Build URLs
    const returnUrl = `${req.protocol}://${req.get('host')}/api/card-linked`;
    const continueUrl = `${req.protocol}://${req.get('host')}/card-success`;

    const formData = payway.generateLinkCardFormParams({
      ctid: userId,
      returnParam: userId,
      firstname,
      lastname,
      email,
      phone,
      returnUrl: encodeUrlToBase64(returnUrl),  // Must be base64 encoded
      continueAddCardSuccessUrl: encodeUrlToBase64(continueUrl)  // Must be base64 encoded
    });

    res.json(formData);
  } catch (error) {
    console.error('Error:', error);
    res.status(500).json({ error: 'Failed to generate form parameters' });
  }
});

// Handle pushback from PayWay (after card linking)
app.post('/api/card-linked', async (req, res) => {
  try {
    const {
      tran_id,
      status,
      token,          // Card token - use this for future purchases
      card_number,    // Masked card number (e.g., "************1234")
      card_type,      // visa, mastercard, jcb, upi
      return_param    // Your original return_param (userId)
    } = req.body;

    console.log('Card linked successfully:', {
      tran_id,
      token,
      card_number,
      card_type,
      userId: return_param
    });

    // Store the card token in your database
    // await db.cards.create({
    //   userId: return_param,
    //   token: token,
    //   maskedNumber: card_number,
    //   cardType: card_type,
    //   tranId: tran_id
    // });

    // Respond to PayWay
    res.status(200).send('OK');
  } catch (error) {
    console.error('Pushback error:', error);
    res.status(500).send('Error');
  }
});

// Success page (user is redirected here after clicking "Done")
app.get('/card-success', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Card Linked Successfully</title>
        <style>
          body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
          }
          .container {
            text-align: center;
            background: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
          }
          h1 { color: #00bcd4; }
          .checkmark {
            font-size: 64px;
            color: #4caf50;
          }
          button {
            margin-top: 20px;
            padding: 10px 30px;
            font-size: 16px;
            background: #00bcd4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
          }
        </style>
      </head>
      <body>
        <div class="container">
          <div class="checkmark">✓</div>
          <h1>Card Linked Successfully</h1>
          <p>Your card has been securely linked to your account.</p>
          <button onclick="window.close()">Close</button>
        </div>
      </body>
    </html>
  `);
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
```

## 2. Next.js API Route Example

```typescript
// app/api/generate-card-form-params/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { PayWayClient } from 'aba-payway-sdk';

const payway = new PayWayClient({
  merchantId: process.env.PAYWAY_MERCHANT_ID!,
  apiKey: process.env.PAYWAY_API_KEY!,
  sandbox: true
});

// Helper function to encode URLs to base64
function encodeUrlToBase64(url: string): string {
  return Buffer.from(url).toString('base64');
}

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const { userId, firstname, lastname, email, phone } = body;

    // Build and encode URLs
    const returnUrl = `${req.nextUrl.origin}/api/card-linked`;
    const continueUrl = `${req.nextUrl.origin}/card-success`;

    // Generate form parameters with hash
    const formData = payway.generateLinkCardFormParams({
      ctid: userId,
      returnParam: userId,
      firstname,
      lastname,
      email,
      phone,
      returnUrl: encodeUrlToBase64(returnUrl),  // Must be base64 encoded
      continueAddCardSuccessUrl: encodeUrlToBase64(continueUrl)  // Must be base64 encoded
    });

    return NextResponse.json(formData);
  } catch (error) {
    console.error('Error:', error);
    return NextResponse.json(
      { error: 'Failed to generate form parameters' },
      { status: 500 }
    );
  }
}
```

```typescript
// app/api/card-linked/route.ts - Pushback handler
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const { tran_id, status, token, card_number, card_type, return_param } = body;

    console.log('Card linked:', {
      tran_id,
      token,
      card_number,
      card_type,
      userId: return_param
    });

    // Store token in database
    await prisma.cardToken.create({
      data: {
        userId: return_param,
        token,
        maskedNumber: card_number,
        cardType: card_type,
        transactionId: tran_id
      }
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Pushback error:', error);
    return NextResponse.json(
      { error: 'Failed to process pushback' },
      { status: 500 }
    );
  }
}
```

## 3. HTML Page with PayWay Modal

Complete HTML page with PayWay's JavaScript library.

**Note**: The example below shows placeholder values. In production, fetch these values (especially `hash`, `return_url`, and `continue_add_card_success_url`) from your backend API which will generate the properly base64-encoded URLs and hash.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Add Card</title>
    
    <!-- PayWay CSS -->
    <link rel="stylesheet" href="https://checkout-sandbox.payway.com.kh/checkout-popup.html?file=css"/>
    
    <!-- PayWay JS -->
    <script src="https://checkout-sandbox.payway.com.kh/checkout-popup.html?file=js"></script>
  </head>
  <body>
    <div class="container">
      <a href="#" id="add_card_button" class="btn btn-primary">Add New Card</a>
    </div>

    <!-- PayWay Modal -->
    <div id="aba_main_modal" class="aba-modal">
      <div class="aba-modal-content add-card">
        <form 
          method="POST" 
          enctype="multipart/form-data"
          target="aba_webservice" 
          id="aba_merchant_add_card" 
          action="https://checkout-sandbox.payway.com.kh/api/payment-gateway/v1/cof/initial?lang=en">
          <input type="hidden" name="merchant_id" value="ec000002" required/>
          <input type="hidden" name="firstname" value="Samnang"/>
          <input type="hidden" name="lastname" value="Sok"/>
          <input type="hidden" name="phone" value="0123456789"/>
          <input type="hidden" name="email" value="sok.samnang@gmail.com"/>
          <input type="hidden" name="ctid" value="239acf04eace99ea1590857c7066acf260e"/>
          <input type="hidden" name="return_param" value="rp-1582083583" required/>
          <input type="hidden" name="hash" value="YOUR_GENERATED_HASH" required/>
          <!-- These URLs MUST be base64 encoded - fetch from backend -->
          <input type="hidden" name="return_url" value="BASE64_ENCODED_URL"/>
          <input type="hidden" name="continue_add_card_success_url" value="BASE64_ENCODED_URL"/>
        </form>
      </div>
    </div>

    <script>
      $(document).ready(function () {
        $('#add_card_button').click(function () {
          AbaPayway.addCard(); // PayWay's modal function
        });
      });
    </script>
  </body>
</html>
```

## 4. Dynamic Form Population

Fetch parameters from backend and populate form dynamically.

```javascript
// Fetch form parameters from backend
fetch('/api/generate-card-form-params', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    userId: 'USER-123',
    firstname: 'Samnang',
    lastname: 'Sok',
    email: 'sok.samnang@example.com',
    phone: '0123456789'
  })
})
.then(res => res.json())
.then(data => {
  // Populate form fields
  const form = document.getElementById('aba_merchant_add_card');
  
  // ✅ Ensure form has correct attributes
  form.action = data.formAction;
  form.enctype = 'multipart/form-data';  // Critical!
  form.method = 'POST';
  
  // Set form inputs (including required merchant_id)
  Object.entries(data.formParams).forEach(([key, value]) => {
    const input = form.querySelector(`[name="${key}"]`);
    if (input) {
      input.value = value;
    } else {
      // Create input if it doesn't exist
      const newInput = document.createElement('input');
      newInput.type = 'hidden';
      newInput.name = key;
      newInput.value = value;
      if (['merchant_id', 'return_param', 'hash'].includes(key)) {
        newInput.required = true;  // Mark required fields
      }
      form.appendChild(newInput);
    }
  });
  
  // Verify required fields are present
  const requiredFields = ['merchant_id', 'return_param', 'hash'];
  const missingFields = requiredFields.filter(field => !form.querySelector(`[name="${field}"]`));
  
  if (missingFields.length > 0) {
    console.error('Missing required fields:', missingFields);
    alert('Form configuration error. Please contact support.');
    return;
  }
  
  // Now user can click "Add Card" button
  document.getElementById('add_card_button').style.display = 'block';
})
.catch(error => {
  console.error('Failed to load form parameters:', error);
  alert('Failed to initialize card form. Please try again.');
});
```

## 5. Complete Working Example with Iframe

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Add Card - PayWay</title>
  
  <!-- PayWay Styles -->
  <link rel="stylesheet" href="https://checkout-sandbox.payway.com.kh/checkout-popup.html?file=css"/>
  
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
    }
    .card-form-container {
      margin-top: 20px;
      border: 1px solid #ddd;
      border-radius: 8px;
      overflow: hidden;
    }
    iframe {
      width: 100%;
      height: 600px;
      border: none;
    }
    button {
      background: #00bcd4;
      color: white;
      border: none;
      padding: 12px 24px;
      font-size: 16px;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover {
      background: #0097a7;
    }
    button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }
  </style>
</head>
<body>
  <h1>Link Your Card</h1>
  <p>Securely link your credit/debit card for faster checkout.</p>
  
  <button id="showCardFormBtn" onclick="showCardForm()">
    Add Credit/Debit Card
  </button>
  
  <div id="cardFormContainer" class="card-form-container" style="display:none;">
    <iframe id="cardFormIframe"></iframe>
  </div>

  <script>
    async function showCardForm() {
      const btn = document.getElementById('showCardFormBtn');
      btn.disabled = true;
      btn.textContent = 'Loading...';

      try {
        // Fetch HTML form from backend
        const response = await fetch('/api/get-card-form-html', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            userId: 'USER-' + Date.now(),
            firstname: 'Samnang',
            lastname: 'Sok',
            email: 'sok.samnang@example.com',
            phone: '0123456789'
          })
        });

        if (!response.ok) {
          throw new Error('Failed to load card form');
        }

        const htmlContent = await response.text();
        
        // Display in iframe
        const iframe = document.getElementById('cardFormIframe');
        iframe.srcdoc = htmlContent;
        
        document.getElementById('cardFormContainer').style.display = 'block';
        btn.textContent = 'Form Loaded';

      } catch (error) {
        console.error('Error:', error);
        alert('Failed to load card form. Please try again.');
        btn.disabled = false;
        btn.textContent = 'Add Credit/Debit Card';
      }
    }
  </script>
</body>
</html>
```

## 6. React Component Example

```typescript
import React, { useState } from 'react';

interface CardLinkingProps {
  userId: string;
  userInfo: {
    firstname: string;
    lastname: string;
    email: string;
    phone: string;
  };
}

export const CardLinking: React.FC<CardLinkingProps> = ({ userId, userInfo }) => {
  const [loading, setLoading] = useState(false);
  const [showForm, setShowForm] = useState(false);
  const [formHtml, setFormHtml] = useState('');

  const loadCardForm = async () => {
    setLoading(true);
    
    try {
      const response = await fetch('/api/generate-card-form-params', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, ...userInfo })
      });

      if (!response.ok) {
        throw new Error('Failed to load card form');
      }

      const formData = await response.json();
      
      // Build HTML form
      const formInputs = Object.entries(formData.formParams)
        .map(([key, value]) => `<input type="hidden" name="${key}" value="${value}" ${['merchant_id', 'return_param', 'hash'].includes(key) ? 'required' : ''}/>`)
        .join('\n');

      const html = `
        <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <link rel="stylesheet" href="https://checkout-sandbox.payway.com.kh/checkout-popup.html?file=css"/>
            <script src="https://checkout-sandbox.payway.com.kh/checkout-popup.html?file=js"></script>
          </head>
          <body>
            <form method="POST" enctype="multipart/form-data" action="${formData.formAction}" target="aba_webservice" id="aba_merchant_add_card">
              ${formInputs}
            </form>
            <script>
              window.onload = () => {
                if (typeof AbaPayway !== 'undefined') {
                  AbaPayway.addCard();
                }
              };
            </script>
          </body>
        </html>
      `;

      setFormHtml(html);
      setShowForm(true);
    } catch (error) {
      console.error('Error:', error);
      alert('Failed to load card form. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button 
        onClick={loadCardForm} 
        disabled={loading}
        className="btn-primary"
      >
        {loading ? 'Loading...' : 'Add New Card'}
      </button>

      {showForm && (
        <div className="card-form-container">
          <iframe 
            srcDoc={formHtml}
            style={{ width: '100%', height: '600px', border: 'none' }}
            title="Card Linking Form"
          />
        </div>
      )}
    </div>
  );
};
```

## 7. Error Handling & Validation Example

```typescript
import { PayWayError } from 'aba-payway-sdk';

// Helper to encode URLs to base64
function encodeUrlToBase64(url: string): string {
  return Buffer.from(url).toString('base64');
}

try {
  // Generate form parameters with hash
  const formData = client.generateLinkCardFormParams({
    returnParam: 'USER-123',
    ctid: '239acf04eace99ea1590857c7066acf260e',
    firstname: 'John',
    lastname: 'Doe',
    email: 'john@example.com',  // Must be valid email format, max 50 chars
    phone: '0123456789',         // Max 20 characters
    returnUrl: encodeUrlToBase64('https://yourapp.com/api/card-linked'),  // Must be base64 encoded
    continueAddCardSuccessUrl: encodeUrlToBase64('https://yourapp.com/success')  // Must be base64 encoded
  });

  console.log('Form action:', formData.formAction);
  console.log('Form params:', formData.formParams);

} catch (error) {
  if (error instanceof PayWayError) {
    console.error('PayWay Error:', error.message);
    console.error('Error Code:', error.code);
    
    // Handle specific error codes
    switch (error.code) {
      case 'NETWORK_ERROR':
        alert('Network connection failed. Please check your internet.');
        break;
      case 'INVALID_MERCHANT':
        alert('Invalid merchant configuration.');
        break;
      default:
        alert('Failed to generate form parameters. Please try again.');
    }
  } else if (error instanceof Error) {
    // Handle validation errors
    console.error('Validation Error:', error.message);
    
    // Common validation errors:
    // - "return_param is required"
    // - "Invalid email format"
    // - "firstname must be <= 50 characters"
    // - "phone must be <= 20 characters"
    // - "return_url must be base64 encoded, not a plain URL"
    // - "continue_add_card_success_url must be base64 encoded, not a plain URL"
    
    if (error.message.includes('email')) {
      alert('Please provide a valid email address.');
    } else if (error.message.includes('required')) {
      alert('Please fill in all required fields.');
    } else if (error.message.includes('characters')) {
      alert('One or more fields exceed the maximum length.');
    } else if (error.message.includes('base64 encoded')) {
      alert('Return URLs must be base64 encoded. Please encode your URL before passing it.');
    } else {
      alert(`Validation error: ${error.message}`);
    }
  } else {
    console.error('Unexpected error:', error);
    alert('An unexpected error occurred.');
  }
}
```

### Validation Rules

The SDK validates the following:

| Field | Validation Rule |
|-------|----------------|
| `returnParam` | Required, max 255 characters |
| `email` | Optional, but must be valid email format if provided, max 50 characters |
| `firstname` | Optional, max 50 characters |
| `lastname` | Optional, max 50 characters |
| `phone` | Optional, max 20 characters |
| `ctid` | Optional, max 255 characters |
| `returnUrl` | Optional, MUST be base64 encoded (not plain URL) |
| `continueAddCardSuccessUrl` | Optional, MUST be base64 encoded (not plain URL) |

## Related Documentation

- Main documentation: [SKILL.md](SKILL.md)
- API reference: [REFERENCE.md](REFERENCE.md)
- Troubleshooting: [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
