npm package: @ojire/payment-gateway-sdk
Description: JavaScript SDK for Ojire Payment Gateway
Author: Ojire
Latest version: 1.0.1

# OPG JavaScript SDK

JavaScript SDK for Ojire Payment Gateway (OPG) - enables secure payment collection via QR Code (QRIS), Virtual Accounts (VA), and Credit Cards (CC).

## Features

- 🔒 **PCI-DSS Compliant**: Credit card data handled through secure Hosted Fields (iFrames)
- 🛡️ **Dual-Key Authentication**: Separate keys for frontend and backend operations
- 💳 **Multiple Payment Methods**: QRIS, Virtual Accounts, Credit Cards
- 🎨 **Customizable UI**: Modal or embedded, light/dark themes
- 📱 **Mobile Responsive**: Works seamlessly on all devices
- 🔄 **Automatic Retries**: Built-in retry logic with exponential backoff
- 🎯 **TypeScript Support**: Full type definitions included
- 📦 **Multiple Formats**: ES modules, CommonJS, and UMD bundles

## Installation

### NPM

```bash
npm install @ojire/opg-sdk
```

### Yarn

```bash
yarn add @ojire/opg-sdk
```

### CDN (UMD)

```html
<!-- Production (minified) -->
<script src="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.min.js"></script>
<link rel="stylesheet" href="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.min.css">

<!-- Development -->
<script src="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.js"></script>
<link rel="stylesheet" href="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.css">
```

## Quick Start

### 1. Create Payment Intent (Backend)

First, create a Payment Intent from your backend using your **Secret Server Key**:

```javascript
// Backend (Node.js example)
const response = await fetch('https://api.ojire.com/v1/payment-intents', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer opg_live_sk_v1_xxxxx', // Secret Server Key
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: {
      value: '150000.00',
      currency: 'IDR'
    },
    order_id: 'ORDER-123',
    customer: {
      name: 'John Doe',
      email: 'john@example.com',
      phone: '+6281234567890'
    },
    description: 'Order #123 - Product Purchase'
  })
});

const { payment_intent } = await response.json();
const clientSecret = payment_intent.client_secret;

// Send clientSecret to your frontend
```

### 2. Initialize SDK (Frontend)

```typescript
import { OPGClient } from '@ojire/opg-sdk';

// Initialize with your Public Client Key
const opg = new OPGClient({
  publicKey: 'opg_test_pk_v1_xxxxx', // Public Client Key
  locale: 'en', // 'en' or 'id'
  theme: 'light' // 'light' or 'dark'
});

// Initialize and check connectivity
await opg.init();
```

### 3. Process Payment

#### Option A: Promise-based API

```typescript
try {
  const result = await opg.pay(clientSecret, {
    modal: true, // Show as modal overlay
    onPending: (result) => {
      console.log('Payment pending:', result);
      // Display QR code or VA number to user
    }
  });
  
  console.log('Payment successful:', result);
  // Redirect to success page
  window.location.href = '/order/success';
  
} catch (error) {
  console.error('Payment failed:', error);
  // Handle error
}
```

#### Option B: Callback-based API

```typescript
const session = await opg.mount({
  clientSecret: clientSecret,
  modal: true,
  
  onSuccess: (result) => {
    console.log('Payment successful:', result);
    // Update UI, redirect to success page
    window.location.href = '/order/success';
  },
  
  onPending: (result) => {
    console.log('Payment pending:', result);
    // Show QR code or VA instructions
  },
  
  onFailure: (error) => {
    console.error('Payment failed:', error);
    // Show error message to user
    alert(`Payment failed: ${error.message}`);
  },
  
  onClose: () => {
    console.log('Payment UI closed');
    // User closed the payment modal
  }
});
```

#### Option C: Embedded in Container

```html
<div id="payment-container"></div>
```

```typescript
const session = await opg.mount({
  clientSecret: clientSecret,
  container: '#payment-container', // or document.getElementById('payment-container')
  modal: false, // Embed in container instead of modal
  
  onSuccess: (result) => {
    console.log('Payment successful:', result);
  }
});
```

## API Reference

### OPGClient

#### Constructor

```typescript
new OPGClient(config: OPGClientConfig | string)
```

**Parameters:**

- `config`: Configuration object or public key string
  - `publicKey` (required): Your Public Client Key
  - `locale` (optional): UI language - `'en'` or `'id'` (default: `'en'`)
  - `theme` (optional): UI theme - `'light'` or `'dark'` (default: `'light'`)
  - `baseUrl` (optional): Custom API base URL (for testing)

**Example:**

```typescript
// Simple initialization
const opg = new OPGClient('opg_test_pk_v1_xxxxx');

// With options
const opg = new OPGClient({
  publicKey: 'opg_test_pk_v1_xxxxx',
  locale: 'id',
  theme: 'dark'
});
```

#### Methods

##### `init(): Promise<void>`

Initialize the SDK and validate connectivity to OPG servers.

```typescript
await opg.init();
```

**Throws:**
- `NetworkError`: If connectivity check fails

##### `pay(clientSecret: string, options?: PaymentOptions): Promise<PaymentResult>`

Shorthand method to mount payment UI and wait for result (Promise-based).

```typescript
const result = await opg.pay(clientSecret, {
  modal: true,
  idempotencyKey: 'unique-key-123', // Optional
  onPending: (result) => {
    // Handle pending state
  }
});
```

**Returns:** `Promise<PaymentResult | PaymentActionRequired>`

##### `mount(options: PaymentOptions): Promise<PaymentSession>`

Mount payment UI and return a PaymentSession for manual control.

```typescript
const session = await opg.mount({
  clientSecret: clientSecret,
  container: '#payment-container',
  modal: false,
  onSuccess: (result) => { /* ... */ },
  onFailure: (error) => { /* ... */ }
});
```

**Returns:** `Promise<PaymentSession>`

##### `destroy(): void`

Cleanup and unmount the SDK.

```typescript
opg.destroy();
```

#### Properties

##### `version: string`

SDK version number.

```typescript
console.log(opg.version); // "1.0.0"
```

### PaymentOptions

Configuration for payment UI and event handlers.

```typescript
interface PaymentOptions {
  clientSecret: string;           // Required: Client secret from Payment Intent
  container?: string | HTMLElement; // DOM selector or element for embedded mode
  modal?: boolean;                 // Show as modal (default: true)
  idempotencyKey?: string;         // Optional: For safe retries
  onSuccess?: (result: PaymentResult) => void;
  onPending?: (result: PendingResult) => void;
  onFailure?: (error: PaymentError) => void;
  onClose?: () => void;
}
```

### PaymentResult

Result object returned on successful payment.

```typescript
interface PaymentResult {
  id: string;                    // Payment ID
  payment_intent_id: string;     // Payment Intent ID
  status: 'success';             // Terminal status
  amount: Amount;                // Payment amount
  payment_method: string;        // 'VA' | 'QRIS' | 'CREDIT_CARD'
  
  // VA-specific fields
  va_number?: string;
  va_bank_code?: string;
  
  // QRIS-specific fields
  qr_code?: string;              // Base64 encoded QR image
  
  // Credit Card-specific fields
  card_last4?: string;
  card_network?: string;
  auth_code?: string;
  
  created_at: string;
  updated_at: string;
}
```

### PendingResult

Result object for pending payments (QR/VA).

```typescript
interface PendingResult {
  id: string;
  payment_intent_id: string;
  status: 'pending';
  amount: Amount;
  payment_method: string;
  
  // Display data for user
  va_number?: string;
  qr_code?: string;
  expires_at?: string;
}
```

### PaymentError

Error object with details.

```typescript
interface PaymentError {
  message: string;
  code: string;
  details?: Record<string, any>;
}
```

### PaymentSession

Active payment session with control methods.

```typescript
class PaymentSession {
  readonly paymentIntentId: string;
  readonly status: PaymentStatus;
  
  // Confirm payment with selected method
  confirm(method: PaymentMethodSelection): Promise<PaymentResult>;
  
  // Get current status
  getStatus(): Promise<PaymentStatus>;
  
  // Start/stop polling for status updates
  startPolling(intervalMs?: number): void;
  stopPolling(): void;
  
  // Close the payment UI
  close(): void;
  
  // Event listeners
  on(event: PaymentEvent, handler: EventHandler): void;
  off(event: PaymentEvent, handler: EventHandler): void;
}
```

## Event Handling

The SDK emits events at key points in the payment lifecycle:

### Event Types

- `success`: Payment completed successfully
- `pending`: Payment requires user action (scan QR, transfer to VA)
- `failure`: Payment failed
- `close`: User closed the payment UI

### Event Data

Each event includes:

```typescript
{
  payment_intent_id: string;
  payment_id?: string;        // Available after confirmation
  status: PaymentStatus;
  // ... additional payment details
}
```

### Example

```typescript
const session = await opg.mount({ clientSecret });

session.on('success', (result) => {
  console.log('Payment ID:', result.payment_id);
  console.log('Amount:', result.amount);
  // Update your UI
});

session.on('pending', (result) => {
  if (result.payment_method === 'QRIS') {
    console.log('Show QR code:', result.qr_code);
  } else if (result.payment_method === 'VA') {
    console.log('VA Number:', result.va_number);
  }
});

session.on('failure', (error) => {
  console.error('Error code:', error.code);
  console.error('Message:', error.message);
});
```

## Error Codes

Common error codes you may encounter:

| Code | Description | Action |
|------|-------------|--------|
| `INVALID_PUBLIC_KEY` | Public key format is invalid | Check your public key format |
| `CONNECTIVITY_ERROR` | Cannot connect to OPG servers | Check network connection |
| `INVALID_CLIENT_SECRET` | Client secret is invalid or expired | Create a new Payment Intent |
| `PAYMENT_EXPIRED` | Payment Intent has expired | Create a new Payment Intent |
| `AMOUNT_MISMATCH` | Payment amount doesn't match intent | Check for tampering |
| `PAYMENT_FAILED` | Payment processing failed | Check payment details |
| `NETWORK_ERROR` | Network request failed | Retry the operation |
| `VALIDATION_ERROR` | Input validation failed | Check input data |
| `CARD_DECLINED` | Card was declined | Try another card |
| `INSUFFICIENT_FUNDS` | Insufficient funds | Use another payment method |
| `3DS_REQUIRED` | 3D Secure authentication required | Complete 3DS challenge |

## Payment Methods

### QRIS (QR Code)

```typescript
const result = await opg.pay(clientSecret, {
  onPending: (result) => {
    // Display QR code to user
    const qrImage = document.getElementById('qr-image');
    qrImage.src = result.qr_code; // Base64 encoded image
    
    // Show expiry time
    console.log('Expires at:', result.expires_at);
  }
});
```

The SDK automatically polls for payment status after QR is displayed.

### Virtual Account (VA)

```typescript
const result = await opg.pay(clientSecret, {
  onPending: (result) => {
    // Display VA number to user
    console.log('Bank:', result.va_bank_code);
    console.log('VA Number:', result.va_number);
    console.log('Amount:', result.amount.value);
    
    // Add copy button
    navigator.clipboard.writeText(result.va_number);
  }
});
```

The SDK automatically polls for payment status after VA is displayed.

### Credit Card

Credit card payments use Hosted Fields for PCI-DSS compliance:

```typescript
import { HostedFields } from '@ojire/opg-sdk';

// Create hosted fields
const hostedFields = new HostedFields({
  cardNumber: { 
    container: '#card-number',
    placeholder: '1234 5678 9012 3456'
  },
  expiryDate: { 
    container: '#expiry-date',
    placeholder: 'MM/YY'
  },
  cvv: { 
    container: '#cvv',
    placeholder: '123'
  },
  styles: {
    base: {
      fontSize: '16px',
      color: '#333'
    },
    invalid: {
      color: '#e74c3c'
    }
  }
});

// Mount the fields
await hostedFields.mount();

// Listen for validation events
hostedFields.on('change', (event) => {
  console.log('Field:', event.field);
  console.log('Valid:', event.valid);
});

// Tokenize and pay
const { token } = await hostedFields.tokenize();

const result = await opg.pay(clientSecret, {
  // Token is automatically used for card payments
});
```

## Security Best Practices

### 1. Never Expose Secret Server Key

❌ **DON'T** use Secret Server Key in frontend:

```javascript
// WRONG - Never do this!
const opg = new OPGClient('opg_live_sk_v1_xxxxx'); // Secret key!
```

✅ **DO** use Public Client Key in frontend:

```javascript
// CORRECT
const opg = new OPGClient('opg_live_pk_v1_xxxxx'); // Public key
```

### 2. Create Payment Intents Server-Side

Always create Payment Intents from your backend to lock the amount:

```javascript
// Backend only
const intent = await createPaymentIntent({
  amount: calculateOrderTotal(order), // Server-side calculation
  order_id: order.id
});

// Send only client_secret to frontend
res.json({ clientSecret: intent.client_secret });
```

### 3. Verify Webhooks

Always verify webhook signatures:

```javascript
// Backend webhook handler
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  const expectedSignature = hmac.digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-opg-signature'];
  const isValid = verifyWebhookSignature(
    req.body,
    signature,
    process.env.WEBHOOK_SECRET
  );
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook
  const { event_type, payment } = req.body;
  // Update order status in database
  
  res.status(200).send('OK');
});
```

### 4. Use HTTPS

Always serve your website over HTTPS in production.

### 5. Implement CSP Headers

Add Content Security Policy headers to prevent XSS:

```
Content-Security-Policy: frame-src https://api.ojire.com https://api-staging.ojire.com;
```

## Testing

### Test Mode

Use test keys for development:

```typescript
const opg = new OPGClient('opg_test_pk_v1_xxxxx');
```

### Test Cards

Use these test card numbers:

| Card Number | Network | Result |
|-------------|---------|--------|
| 4111111111111111 | Visa | Success |
| 5555555555554444 | Mastercard | Success |
| 378282246310005 | Amex | Success |
| 4000000000000002 | Visa | Declined |
| 4000000000009995 | Visa | Insufficient Funds |

### Test VA Banks

Available test banks:
- BCA: `014`
- BNI: `009`
- Mandiri: `008`
- BRI: `002`

### Test QRIS

In test mode, QRIS payments are automatically marked as successful after 5 seconds.

## Browser Support

- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
- Mobile browsers (iOS Safari 14+, Chrome Mobile 90+)

## TypeScript Support

The SDK is written in TypeScript and includes full type definitions:

```typescript
import { 
  OPGClient, 
  PaymentResult, 
  PaymentError,
  OPGClientConfig 
} from '@ojire/opg-sdk';

const config: OPGClientConfig = {
  publicKey: 'opg_test_pk_v1_xxxxx',
  locale: 'en',
  theme: 'light'
};

const opg = new OPGClient(config);

try {
  const result: PaymentResult = await opg.pay(clientSecret);
  console.log(result.payment_id);
} catch (error) {
  const paymentError = error as PaymentError;
  console.error(paymentError.code);
}
```

## Migration Guide

### From v0.x to v1.0

1. Update initialization:

```typescript
// Old
const opg = new OPG({ apiKey: 'xxx' });

// New
const opg = new OPGClient({ publicKey: 'opg_test_pk_v1_xxx' });
await opg.init();
```

2. Update payment method:

```typescript
// Old
opg.createPayment({ amount: 150000 });

// New
// Create Payment Intent from backend first
const result = await opg.pay(clientSecret);
```

## Examples

See the `/examples` directory for complete examples:

- [Basic Payment](./examples/basic-payment.html)
- [React Integration](./examples/react-app/)
- [Vue Integration](./examples/vue-app/)
- [Next.js Integration](./examples/nextjs-app/)

## Support

- Documentation: https://docs.ojire.com
- API Reference: https://api-docs.ojire.com
- Support Email: support@ojire.com
- GitHub Issues: https://github.com/ojire/opg-sdk/issues

## Development

```bash
# Install dependencies
npm install

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Build
npm run build

# Type check
npm run typecheck

# Lint
npm run lint
```

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.

## License

MIT License - see [LICENSE](./LICENSE) for details.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for version history.

---

Made with ❤️ by Ojire