# InflowPay React SDK

Accept card payments with a pre-built, secure payment form for React applications.

## Installation

```bash
npm install inflowpay-js react react-dom
```

## Quick Start

```tsx
import { InflowPayProvider, CardElement } from 'inflowpay-js/react';

function App() {
  const paymentId = 'pay_xxx'; // From your backend

  return (
    <InflowPayProvider config={{ apiKey: 'inflow_pub_xxx' }}>
      <CardElement
        paymentId={paymentId}
        onComplete={(result) => {
          if (result.status === 'CHECKOUT_SUCCESS') {
            window.location.href = '/success';
          }
        }}
      />
    </InflowPayProvider>
  );
}
```

**That's it!** The form handles card validation, tokenization, 3DS, and payment completion.

## Features

- 🔒 **PCI Compliant** - Card data never touches your server
- 🎨 **Customizable** - Match your brand with custom styling
- 🔐 **3DS Built-in** - Automatic 3D Secure authentication
- 📱 **Mobile Ready** - Responsive on all devices
- 💪 **TypeScript** - Full type definitions included

## How It Works

1. **Backend**: Create a payment with billing info (your private key)
2. **Frontend**: Wrap your app with `InflowPayProvider`
3. **Frontend**: Add `CardElement` component with the `paymentId`
4. **User**: Enters card details and submits
5. **SDK**: Handles tokenization, 3DS, and payment completion
6. **Your app**: Receives success/error via callback

## Payment Status

Payments progress through these statuses:

| Status | Description | Success? |
|--------|-------------|----------|
| `CHECKOUT_SUCCESS` | Payment confirmed | ✅ Yes |
| `PAYMENT_RECEIVED` | Funds received | ✅ Yes |
| `PAYMENT_SUCCESS` | Complete | ✅ Yes |
| `PAYMENT_FAILED` | Failed | ❌ No |

**Check for success:**
```tsx
if (result.status === 'CHECKOUT_SUCCESS' ||
    result.status === 'PAYMENT_RECEIVED' ||
    result.status === 'PAYMENT_SUCCESS') {
  // Payment successful
}
```

## Customization

### Styling

The CardElement supports comprehensive styling through the `style` prop. All styles are scoped to the card element using Shadow DOM, so they won't conflict with your page styles.

#### Basic Styling Example

```tsx
<CardElement
  paymentId={paymentId}
  style={{
    // Input container (the box wrapping all inputs)
    inputContainer: {
      backgroundColor: '#F5F5F5',
      border: '1px solid #E0E0E0',
      borderRadius: '8px',
    },
    // Individual input fields
    input: {
      border: '1px solid #CECECE',
      borderRadius: '5px',
      color: '#333333',
      fontWeight: '400',
      placeholder: {
        color: '#999999',
      },
    },
    // Global font family
    fontFamily: "'Inter', sans-serif",
  }}
/>
```

#### Complete Styling Reference

##### `style` prop - CSSProperties

```typescript
interface CSSProperties {
  // Input container styles (the box wrapping all 3 input fields)
  inputContainer?: {
    backgroundColor?: string;  // Background color of the container
    border?: string;            // Border (e.g., '1px solid #E0E0E0')
    borderRadius?: string;       // Border radius (e.g., '8px')
  };

  // Individual input field styles
  input?: {
    border?: string;            // Input border (e.g., '1px solid #CECECE')
    borderRadius?: string;       // Input border radius (e.g., '5px')
    color?: string;             // Text color
    fontWeight?: string;        // Font weight (e.g., '400', '500', 'bold')
    placeholder?: {
      color?: string;           // Placeholder text color
    };
  };

  // Global font family (applies to entire card element)
  fontFamily?: string;         // Font family (e.g., "'Inter', sans-serif")

  // Dark mode overrides (optional)
  // Automatically applies when system is in dark mode
  dark?: {
    inputContainer?: { backgroundColor?: string; border?: string; borderRadius?: string; };
    input?: { border?: string; borderRadius?: string; color?: string; fontWeight?: string; placeholder?: { color?: string; }; };
  };
}
```

**Example with dark mode override:**
```tsx
<CardElement
  paymentId={paymentId}
  style={{
    inputContainer: {
      backgroundColor: '#F5F5F5',
      border: '1px solid #E0E0E0',
      borderRadius: '8px',
    },
    input: {
      border: '1px solid #CECECE',
      borderRadius: '5px',
      color: '#333333',
      fontWeight: '400',
      placeholder: {
        color: '#999999',
      },
    },
    fontFamily: "'Inter', sans-serif",
    // Dark mode overrides (applies when system is in dark mode)
    dark: {
      inputContainer: {
        backgroundColor: '#2d2d2d',
        border: '1px solid #7A7A7A',
      },
      input: {
        color: '#FFFFFF',
        border: '1px solid #7A7A7A',
        placeholder: {
          color: '#959499',
        },
      },
    },
  }}
/>
```

**Note:** The `dark` theme override automatically applies when the system is in dark mode.

##### `buttonStyle` prop - ButtonStyle | Partial<CSSStyleDeclaration>

Two ways to style the button:

**Option 1: Flat structure (simple CSS properties)**
```tsx
<CardElement
  paymentId={paymentId}
  buttonStyle={{
    backgroundColor: '#007bff',
    color: '#ffffff',
    borderRadius: '6px',
    padding: '12px 24px',
    fontSize: '16px',
    fontWeight: '500',
  }}
/>
```

**Option 2: Structured with states**
```tsx
<CardElement
  paymentId={paymentId}
  buttonStyle={{
    base: {
      backgroundColor: '#007bff',
      color: '#ffffff',
      borderRadius: '6px',
      padding: '12px 24px',
      fontSize: '16px',
      fontWeight: '500',
    },
    hover: {
      backgroundColor: '#0056b3',
      transform: 'translateY(-1px)',
    },
    disabled: {
      backgroundColor: '#cccccc',
      cursor: 'not-allowed',
    },
    loaderColor: '#ffffff',  // Color of the loading spinner
  }}
/>
```

**ButtonStyle interface:**
```typescript
interface ButtonStyle {
  base?: Partial<CSSStyleDeclaration>;      // Base button styles
  hover?: Partial<CSSStyleDeclaration>;     // Hover state styles
  disabled?: Partial<CSSStyleDeclaration>;  // Disabled state styles
  loaderColor?: string;                     // Loading spinner color
}
```

##### `buttonText` prop

Customize the button text:

```tsx
<CardElement
  buttonText="Complete Payment"  // Default: "Complete Payment"
/>
```

##### `placeholders` prop

Customize placeholder text for each input:

```tsx
<CardElement
  placeholders={{
    cardNumber: '1234 5678 9012 3456',  // Default: '4242 4242 4242 4242'
    expiry: 'MM/YY',                     // Default: 'MM/YY'
    cvc: '123',                          // Default: 'CVC'
  }}
/>
```

### Callbacks

```tsx
<CardElement
  paymentId={paymentId}
  onReady={() => {
    // Card element is mounted and ready
    console.log('Card form is ready');
  }}
  onChange={(state) => {
    // Track validation state
    console.log('Valid:', state.complete);
  }}
  onComplete={(result) => {
    // Payment complete (success or failure)
    if (result.error) {
      alert(result.error.message);
    }
  }}
  onError={(error) => {
    // SDK-level errors (network, validation)
    console.error(error);
  }}
/>
```

## 3D Secure

3DS is handled automatically. When required:
1. SDK opens a modal with the authentication page
2. User completes authentication
3. Modal closes automatically
4. Your `onComplete` callback receives the final result

### Setup

Copy `3ds-callback.html` to your public directory:

```bash
cp node_modules/inflowpay-js/templates/3ds-callback.html public/
```

The file must be accessible at `https://yourdomain.com/3ds-callback.html` for the 3DS authentication flow to work.

**That's it!** The SDK automatically uses this URL for 3DS redirects.

### What This File Does

- Receives the redirect from the 3DS authentication provider
- Shows a success/failure message to the user
- Sends the result back to the parent modal via postMessage
- Auto-closes after completion

**Important notes:**
- The file must be on the same origin as your application (same domain, protocol, and port)
- Do not modify the file unless you need custom branding
- The file is small (~4KB) and has no external dependencies

### Local Development with 3DS

**Important:** To test 3DS locally, you MUST serve your application through a public HTTPS tunnel. Modern browsers block 3DS providers from redirecting to localhost due to Private Network Access security restrictions.

**Setup:**

1. Serve your application on a local port (e.g., port 5173 for Vite):

```bash
npm run dev  # Vite runs on port 5173
```

2. Tunnel that port to a public HTTPS URL:

```bash
# Option 1: localtunnel (simple, no signup)
npx localtunnel --port 5173
# Returns: https://random-subdomain.loca.lt

# Option 2: ngrok (more reliable, requires free account)
npx ngrok http 5173
# Returns: https://abc123.ngrok.io
```

3. Access your app via the tunnel URL (not localhost):

- ✅ Use: `https://random-subdomain.loca.lt`
- ❌ Don't use: `http://localhost:5173`

The SDK will automatically use the tunnel URL for the 3DS callback (`https://random-subdomain.loca.lt/3ds-callback.html`), and the 3DS provider will be able to redirect to it successfully.

**Why this is required:**
- 3DS providers (Basis Theory, Stripe) run on public servers
- After authentication, they redirect to your callback URL
- Browsers block redirects from public → private (localhost) networks
- A tunnel creates a public HTTPS URL that forwards to your localhost

## Error Handling

Errors are automatically mapped to user-friendly messages:

```tsx
<CardElement
  paymentId={paymentId}
  onComplete={(result) => {
    if (result.error) {
      console.log(result.error.message); // User-friendly
      console.log(result.error.retryable); // Can retry?
    }
  }}
/>
```

**Common errors:**
- `card_declined` - Card was declined
- `insufficient_funds` - Not enough funds
- `invalid_card_number` - Invalid card
- `THREE_DS_FAILED` - 3DS authentication failed

## API Reference

### InflowPayProvider

```tsx
<InflowPayProvider config={SDKConfig}>
  {children}
</InflowPayProvider>
```

**Config:**
- `apiKey` (required) - Your public key (`inflow_pub_xxx`)
- `locale` (optional) - UI text language (default: auto-detected from browser)
  - Supported: `'en'`, `'de'`, `'es'`, `'fr'`, `'it'`, `'nl'`, `'pl'`, `'pt'`
- `timeout` (optional) - Request timeout in ms (default: 30000)
- `debug` (optional) - Enable logging (default: false)

**Locale example:**
```tsx
// Explicit locale (French)
<InflowPayProvider config={{ apiKey: 'inflow_pub_xxx', locale: 'fr' }}>
  <CardElement paymentId={paymentId} />
</InflowPayProvider>

// Auto-detect from browser (default)
<InflowPayProvider config={{ apiKey: 'inflow_pub_xxx' }}>
  <CardElement paymentId={paymentId} />
</InflowPayProvider>
```


### CardElement

```tsx
<CardElement
  paymentId={string}
  config?: SDKConfig  // Optional if using InflowPayProvider
  buttonText?: string
  buttonStyle?: ButtonStyle | Partial<CSSStyleDeclaration>
  style?: CSSProperties
  placeholders?: {
    cardNumber?: string
    expiry?: string
    cvc?: string
  }
  onReady?: () => void
  onChange?: (state: CardElementState) => void
  onComplete?: (result: PaymentResult) => void
  onError?: (error: PaymentError) => void
/>
```

### useInflowPay Hook

Access the InflowPay instance from context:

```tsx
import { useInflowPay } from 'inflowpay-js/react';

function CustomComponent() {
  const inflow = useInflowPay();
  
  // Use inflow methods if needed
  const checkStatus = async () => {
    const status = await inflow.getPaymentStatus('pay_xxx');
    console.log(status);
  };
  
  return <button onClick={checkStatus}>Check Status</button>;
}
```

## TypeScript

Full type definitions included:

```tsx
import { InflowPayProvider, CardElement } from 'inflowpay-js/react';
import type { PaymentResult, PaymentError } from 'inflowpay-js';

function App() {
  return (
    <InflowPayProvider config={{ apiKey: 'inflow_pub_xxx' }}>
      <CardElement
        paymentId="pay_xxx"
        onComplete={(result: PaymentResult) => {
          if (result.status === 'CHECKOUT_SUCCESS') {
            // Success
          }
        }}
        onError={(error: PaymentError) => {
          console.error(error);
        }}
      />
    </InflowPayProvider>
  );
}
```

## Backend Integration

Create payments on your backend with your **private key**:

```javascript
// Your backend (Node.js example)
const response = await fetch('https://api.inflowpay.xyz/api/server/payment', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'X-Inflow-Api-Key': 'inflow_priv_xxx' // Private key
  },
  body: JSON.stringify({
    products: [{ name: 'Product', price: 4999, quantity: 1 }],
    currency: 'EUR',
    customerEmail: 'customer@example.com',
    billingCountry: 'FR',
    postalCode: '75001',
    firstName: 'John',
    lastName: 'Doe',
    purchasingAsBusiness: false,
    successUrl: 'https://yourdomain.com/success',
    cancelUrl: 'https://yourdomain.com/cancel'
  })
});

const { payment } = await response.json();
// Send payment.id to your frontend
```

See [API documentation](https://docs.inflowpay.com/v0/reference/createpayment) for full backend API reference.

## Security

- ✅ Card data is tokenized before reaching your server (PCI compliant)
- ✅ Public keys can be safely exposed in browser
- ✅ Private keys should never be in frontend code
- ✅ All requests over HTTPS

## Support

- **Documentation:** https://docs.inflowpay.com/v0/reference/createpayment
- **Email:** support@inflowpay.com
- **GitHub:** https://github.com/inflowpay/sdk

## License

MIT - See [LICENSE](./LICENSE)
