# Checkout React Native Components

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

- [🚀 Quick Start](#-quick-start)
  - [Installation](#installation)
  - [Platform Setup](#platform-setup)
- [🧩 Components Overview](#-components-overview)
- [⚙️ Core Configuration](#️-core-configuration)
  - [CheckoutProvider Setup](#checkoutprovider-setup)
  - [Configuration Options](#-configuration-options)
- [📖 Component Reference](#-component-reference)
  - [Flow Component](#flow-component)
  - [Card Component](#card-component)
  - [Apple Pay Component](#apple-pay-component)
  - [Google Pay Component](#google-pay-component)
- [🔧 Advanced Features](#-advanced-features)
  - [Imperative API](#imperative-api)
- [🎨 Customization](#-customization)
  - [Color Tokens](#-complete-color-token-reference)
  - [Border Radius](#-border-radius)
  - [Localization](#-supported-locales--custom-translations)
- [📡 Event Handling](#-event-handling)
  - [Component Callbacks](#-complete-event-reference)
  - [Error Types](#error-types)
  - [Tokenization Result](#tokenization-result)
  - [Handle Submit](#handle-submit)
- [🔍 Linting & formatting](#-linting--formatting)
- [🪝 Git hooks (Lefthook)](#-git-hooks-lefthook)
- [📄 License](#-license)

For detailed integration steps, refer to our 
<kbd>[official documentation](https://www.checkout.com/docs/payments/accept-payments/accept-a-payment-on-your-mobile-app)  ↗️ </kbd>.

## 🚀 Quick Start

### Installation

Install the package using your preferred package manager:

```sh
npm install @checkout.com/checkout-react-native-components
```
```sh
yarn add @checkout.com/checkout-react-native-components
```

### Platform Setup

This package supports both React Native architectures (New Architecture/Fabric and Old Architecture/Paper) out of the box.

#### 📱 iOS Setup

```bash
cd ios
pod install
```

Depending on which version of `checkout-react-native-components` you use, you may need to pin FingerprintPro to either `2.12.0` or above. You can do this by manually modifying your Podfile with the following:
```ruby
  pod 'FingerprintPro', '2.12.0'
```

For expo, use the `expo-build-properties` plugin:

```json
  [
    "expo-build-properties",
    {
      "ios": {
        "extraPods": [
          {
            "name": "FingerprintPro",
            "version": "2.12.0"
          }
        ]
      }
    }
  ]
```

#### 🤖 Android Setup

Add the following repositories to your app's `settings.gradle`:

```groovy
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
        maven { url = uri("https://maven.fpregistry.io/releases") }
    }
}
```

Or add the `allProjects` block to the project `build.gradle`:

```groovy
allprojects {
  repositories {
    google()
    mavenCentral()
    maven { url 'https://maven.fpregistry.io/releases' }
    maven { url 'https://www.jitpack.io' }
  }
}
```

To enable Google Pay payments the following metadata tag must be added to your `AndroidManifest.xml`:

```xml
<meta-data
    android:name="com.google.android.gms.wallet.api.enabled"
    android:value="true" />
```

While not mandatory, we recommend setting `android:allowBackup` to `false` in your `AndroidManifest.xml`. Failing to set it to `false` or to override our default value with your own will result in a manifest merge error.

**Requirements:**
- **New Arch:** Android Studio Otter 3 Feature Drop 2025.2.3
- **Old Arch:** Android Studio Narwhal

💡 **Tip:** Use [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) for easy Android Studio version management.

#### Expo Setup

Our package cannot run in Expo Go. Instead, you need to run:
```bash
npx expo prebuild
```
This command compiles your app with our package included.

For Android, the repositories step above is a hard requirement to resolve dependencies. Our package provides a general-purpose plugin that appends the repositories defined in the Android Setup block above. The plugin can also append the Google Pay tag to the `AndroidManifest.xml` if required.

If your app already uses custom plugins that modify `build.gradle` or `AndroidManifest.xml`, we recommend extending your existing plugin or creating an additional plugin to customize adding the repositories block to fit your use case. You can find documentation on writing Expo plugins [here](https://docs.expo.dev/config-plugins/plugins/).

The `android:allowBackup` tag and `plugins` can be set in `app.json` like so:

```json
"android": {
  "allowBackup": false
},
"plugins": [
  [
    "@checkout.com/checkout-react-native-components",
    {
      "enableGooglePay": true
    }
  ]
]
```

## 🧩 Components Overview

| Component | Description | Platform | Key Features |
|-----------|-------------|----------|--------------|
| **Flow** | Complete payment flow with multiple payment methods | iOS/Android | 3DS support, dynamic payment methods, built-in validation |
| **Card** | Secure card input form | iOS/Android | Card validation, tokenization, custom styling |
| **Apple Pay** | Native Apple Pay integration | iOS only | Seamless iOS payment experience |
| **Google Pay** | Native Google Pay integration | Android only | Seamless Android payment experience |

## ⚙️ Core Configuration

### CheckoutProvider Setup

Wrap your payment screen with `CheckoutProvider` to enable payment functionality.

Create a [Payment Session](https://www.checkout.com/docs/payments/accept-payments/accept-a-payment-on-your-mobile-app/get-started-with-flow-for-mobile#Step_1:_Create_a_new_Payment_Session_) and pass it to `CheckoutProvider`.

```typescript
import {
  CheckoutProvider,
  CheckoutConfiguration,
  Environment,
  Locale
} from '@checkout.com/checkout-react-native-components';

const config: CheckoutConfiguration = {
  publicKey: 'pk_test_your_public_key',
  environment: 'sandbox', // or 'production'
  locale: Locale.en_GB,
  merchantIdentifier: 'merchant.com.your-app.name', // Required for Apple Pay
  translations: {
    [Locale.en_GB]: {
      cardNumber: 'Card Number',
      payButtonPay: 'Pay Now'
    }
  },
  style: {
    colorTokens: {
      primary: '#186AFF',
      background: '#FFFFFF',
      border: '#E3E8F2',
      error: '#E01E5A'
    },
    borderButtonRadius: 12,
    borderFormRadius: 8
  }
};

export default function PaymentScreen() {
  const [paymentSession, setPaymentSession] = useState({
    id: '',
    payment_session_secret: ''
  });

  useEffect(() => {
    fetchPaymentSession().then((data) => {
      setPaymentSession(data);
    });
  }, []);

  return (
    <CheckoutProvider 
      config={config}
      onSuccess={(paymentMethod, paymentID) => {
        console.log('Payment successful:', { paymentMethod, paymentID });
      }}
      onError={(error) => {
        console.error('Payment error:', error);
      }}
      paymentSession={paymentSession}
    >
      {/* Your app components */}
    </CheckoutProvider>
  );
}
```

### 📝 Configuration Options

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `publicKey` | `string` | ✅ | Your Checkout.com public API key |
| `environment` | `'sandbox' \| 'production'` | ✅ | Environment for API calls |
| `locale` | `string` | ❌ | Locale for UI text (defaults to English) |
| `merchantIdentifier` | `string` | ❌ | Apple Pay merchant identifier |
| `translations` | `Record<string, Translations>` | ❌ | Custom text translations |
| `style` | `Style` | ❌ | UI customization options |

## 📖 Component Reference

### Flow Component

The **Flow** component provides a complete payment experience with support for multiple payment methods, 3D Secure, and dynamic payment method display.

### 🔧 Flow Configuration & Usage

#### Basic Usage

```typescript
import { Flow } from '@checkout.com/checkout-react-native-components';

const PaymentScreen = () => {
  return (
    <CheckoutProvider config={config} paymentSession={paymentSession}>
      <Flow style={{ marginVertical: 20 }} />
    </CheckoutProvider>
  );
};
```

#### Imperative API Usage

```typescript
const PaymentScreen = () => {
  const flowRef = useRef<FlowRef>(null);

  const handleSubmit = () => {
    flowRef.current?.submit(); // Process payment
  };

  const handleTokenize = () => {
    flowRef.current?.tokenize(); // Tokenize card
  };

  const updateDetails = (amount: number, currency: string | undefined) => {
    // Dynamically adjust the payment amount
    flowRef.current?.update({amount: amount, currency: currency });
  }

  return (
    <CheckoutProvider config={config} paymentSession={paymentSession}>
      <Flow 
        ref={flowRef}
        config={{ showPayButton: false }} // Hide built-in pay button
      />
      <Button title="Pay" onPress={handleSubmit} />
      <Button title="Tokenize Card" onPress={handleTokenize} />
    </CheckoutProvider>
  );
};
```

#### Flow Props

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `config` | `FlowConfig` | ❌ | Flow-specific configuration |
| `style` | `ViewStyle \| ViewStyle[]` | ❌ | React Native styling |
| `ref` | `FlowRef` | ❌ | Imperative API reference |

#### FlowConfig Options

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `showPayButton` | `boolean` | `true` | Show/hide the built-in pay button |
| `paymentButtonAction` | `'payment' \| 'tokenize'` | `'payment'` | Action when pay button is pressed |

### Card Component

The **Card** component provides a secure card input form ideal for custom checkout implementations.

### 💳 Card Configuration & Usage

#### Basic Usage

```typescript
import { Card, CardRef } from '@checkout.com/checkout-react-native-components';

const CardPaymentScreen = () => {
  const cardRef = useRef<CardRef>(null);
  
  const handleSubmit = () => {
    cardRef.current?.submit();
  };

  return (
    <CheckoutProvider config={config} paymentSession={paymentSession}>
      <Card 
        ref={cardRef}
        config={{ showPayButton: false }}
      />
    </CheckoutProvider>
  );
};
```

#### Card Props

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `config` | `CardConfig` | ❌ | Card-specific configuration |
| `style` | `ViewStyle \| ViewStyle[]` | ❌ | React Native styling |
| `ref` | `CardRef` | ❌ | Imperative API reference |

#### CardConfig Options

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `showPayButton` | `boolean` | `true` | Show/hide the built-in pay button |
| `paymentButtonAction` | `'payment' \| 'tokenize'` | `'payment'` | Action when pay button is pressed |

### Apple Pay Component

The **Apple Pay** component provides native Apple Pay integration for iOS devices.

### 🍎 Apple Pay Configuration & Usage

#### Basic Usage

```typescript
import { ApplePay, CheckoutProvider } from '@checkout.com/checkout-react-native-components';

const PaymentScreen = () => {
  return (
    <CheckoutProvider 
      config={{
        ...config,
        merchantIdentifier: 'merchant.com.your-app.name' // Required!
      }} 
      paymentSession={paymentSession}
    >
      <ApplePay style={{ height: 60, marginVertical: 10 }} />
    </CheckoutProvider>
  );
};
```

#### Apple Pay Props

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `style` | `ViewStyle \| ViewStyle[]` | ❌ | React Native styling |

**📝 Note:** Apple Pay requires a valid `merchantIdentifier` in your `CheckoutConfiguration` and is only available on iOS devices.

### Google Pay Component

The **Google Pay** component provides native Google Pay integration for Android devices.

### 🤖 Google Pay Configuration & Usage

#### Basic Usage

```typescript
import { CheckoutProvider, GooglePay } from '@checkout.com/checkout-react-native-components';

const PaymentScreen = () => {
  return (
    <CheckoutProvider config={config} paymentSession={paymentSession}>
      <GooglePay />
    </CheckoutProvider>
  );
};
```

#### Google Pay Props

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `config` | `GooglePayConfig` | ❌ | Google Pay configuration (reserved for future use) |
| `style` | `ViewStyle \| ViewStyle[]` | ❌ | React Native styling |

**📝 Note:** Google Pay is only available on Android devices.

## 🔧 Advanced Features

### Imperative API

Both `Flow` and `Card` components support imperative APIs (submit and token) for programmatic control:

```typescript
import { CheckoutProvider, Card, Flow, FlowRef, CardRef } from '@checkout.com/checkout-react-native-components';

const PaymentScreen = () => {
  const flowRef = useRef<FlowRef>(null);
  const cardRef = useRef<CardRef>(null);

  // Trigger payment submission programmatically
  const processPayment = () => {
    flowRef.current?.submit();
  };

  // Tokenize card details without submitting
  const tokenizeCard = () => {
    cardRef.current?.tokenize();
  };

  // Update payment details (Wallet Payment Components only)
  const updateDetails = (amount: number, currency: string) => {
    flowRef.current?.update({ amount, currency });
  };

  return (
    <CheckoutProvider config={config} paymentSession={paymentSession}>
      <Flow ref={flowRef} config={{ showPayButton: false }} />
      <Card ref={cardRef} config={{ showPayButton: false }} />
      
      <Button title="Process Payment" onPress={processPayment} />
      <Button title="Save Card" onPress={tokenizeCard} />
    </CheckoutProvider>
  );
};
```

## 🎨 Customization

### 🎨 Complete Color Token Reference

```typescript
const customStyle = {
  colorTokens: {
    // Primary brand color
    primary: '#186AFF',
    
    // Background colors
    background: '#FFFFFF',
    formBackground: '#F7F9FC',
    
    // Border colors
    border: '#E3E8F2',
    formBorder: '#C8D1E0',
    outline: '#91B2EE',
    
    // State colors
    error: '#E01E5A',
    success: '#1AA27B',
    disabled: '#B0B0B0',
    
    // Text colors
    secondary: '#545F7A',
    inverse: '#FFFFFF',
    
    // Action colors
    action: '#186AFF',
    
    // Android specific
    scrolledContainer: '#F0F0F0' // Android only
  }
};
```

### 📏 Border Radius

```typescript
const customBorders = {
  borderButtonRadius: 12,  // Button border radius
  borderFormRadius: 8      // Form field border radius
};
```

### 🔤 Custom Fonts

```typescript
const fonts = {
  button: {
    fontFamily: 'Custom-Font',
    fontSize: 14,
    letterSpacing: 2,
    lineHeight: 8,
  },
  input: {
    fontFamily: 'Custom-Font',
    fontSize: 14,
    letterSpacing: 2,
    lineHeight: 8,
  },
  footnote: {
    fontFamily: 'Custom-Font',
    fontSize: 14,
    letterSpacing: 2,
    lineHeight: 8,
  },
  label: {
    fontFamily: 'Custom-Font',
    fontSize: 14,
    letterSpacing: 2,
    lineHeight: 8,
  },
  subheading: {
    fontFamily: 'Custom-Font',
    fontSize: 14,
    letterSpacing: 2,
    lineHeight: 8,
  },
};
```

#### iOS

The SDK supports all iOS default system [fonts](https://developer.apple.com/fonts/system-fonts/)

#### Android

The following built-in Android font families are supported:
- SansSerif
- Serif
- Monospace
- Cursive
- Default

### 🌍 Supported Locales & Custom Translations

#### Supported Locales

```typescript
enum Locale {
  ar = 'ar',           // Arabic
  da_DK = 'da-DK',     // Danish
  de_DE = 'de-DE',     // German
  el = 'el',           // Greek
  en_GB = 'en-GB',     // English (UK)
  es_ES = 'es-ES',     // Spanish
  fi_FI = 'fi-FI',     // Finnish
  fil_PH = 'fil-PH',   // Filipino
  fr_FR = 'fr-FR',     // French
  hi_IN = 'hi-IN',     // Hindi
  id_ID = 'id-ID',     // Indonesian
  it_IT = 'it-IT',     // Italian
  ja_JP = 'ja-JP',     // Japanese
  ms_MY = 'ms-MY',     // Malay
  nb_NO = 'nb-NO',     // Norwegian
  nl_NL = 'nl-NL',     // Dutch
  pt_PT = 'pt-PT',     // Portuguese
  sv_SE = 'sv-SE',     // Swedish
  th_TH = 'th-TH',     // Thai
  vi_VN = 'vi-VN',     // Vietnamese
  zh_CN = 'zh-CN',     // Chinese (Simplified)
  zh_HK = 'zh-HK',     // Chinese (Hong Kong)
  zh_TW = 'zh-TW'      // Chinese (Traditional)
}
```

#### Translations Interface

All available translation keys with their descriptions:

```typescript
export type Translations = {
  addBillingAddress?: string;
  addAddress?: string;
  address?: string;
  addressLine1?: string;
  addressLine2?: string;
  billingAddress?: string;
  card?: string;
  cardExpiryDate?: string;
  cardExpiryDateIncomplete?: string;
  cardExpiryDateInvalid?: string;
  cardExpiryDatePlaceholderMonth?: string;
  cardExpiryDatePlaceholderYear?: string;
  cardHolderName?: string;
  cardNumber?: string;
  cardNumberInvalid?: string;
  cardNumberNotSupported?: string;
  cardSecurityCode?: string;
  cardSecurityCodeInvalid?: string;
  cardSecurityCodePlaceholder?: string;
  city?: string;
  confirm?: string;
  country?: string;
  selectCountry?: string;
  editAddress?: string;
  email?: string;
  emailFormatInvalid?: string;
  firstName?: string;
  formRequired?: string;
  lastName?: string;
  insufficientCharacters?: string;
  noMatchesFound?: string;
  optional?: string;
  payButtonPay?: string;
  payButtonPaymentComplete?: string;
  payButtonPaymentProcessing?: string;
  paymentDeclinedInvalidCustomerData?: string;
  paymentDeclinedInvalidPaymentSessionData?: string;
  paymentDeclinedMerchantMisconfiguration?: string;
  paymentDeclinedNotEnoughFunds?: string;
  paymentDeclinedTryAgain?: string;
  phoneNumber?: string;
  search?: string;
  state?: string;
  trySearchingWithAnotherTerm?: string;
  useShippingAsBilling?: string;
  zip?: string;
  preferredSchemeCta?: string;
  preferredSchemeDescription?: string;
  selectState?: string;
};
```

#### Custom Translations Example

```typescript
const customTranslations = {
  [Locale.en_GB]: {
    // Address and billing
    addBillingAddress: 'Add Billing Address',
    addAddress: 'Add Address',
    address: 'Address',
    addressLine1: 'Address Line 1',
    addressLine2: 'Address Line 2',
    billingAddress: 'Billing Address',
    
    // Card form labels
    card: 'Card',
    cardNumber: 'Card Number',
    cardExpiryDate: 'MM/YY',
    cardExpiryDatePlaceholderMonth: 'MM',
    cardExpiryDatePlaceholderYear: 'YY',
    cardSecurityCode: 'CVV',
    cardSecurityCodePlaceholder: 'CVV',
    cardHolderName: 'Cardholder Name',
    
    // Personal information
    firstName: 'First Name',
    lastName: 'Last Name',
    email: 'Email Address',
    phoneNumber: 'Phone Number',
    
    // Location fields
    city: 'City',
    state: 'State/Province',
    zip: 'ZIP/Postal Code',
    country: 'Country',
    selectCountry: 'Select Country',
    selectState: 'Select State',
    
    // Actions and buttons
    payButtonPay: 'Pay Now',
    payButtonPaymentProcessing: 'Processing...',
    payButtonPaymentComplete: 'Payment Complete',
    confirm: 'Confirm',
    editAddress: 'Edit Address',
    search: 'Search',
    
    // Validation and error messages
    formRequired: 'This field is required',
    cardNumberInvalid: 'Invalid card number',
    cardNumberNotSupported: 'Card type not supported',
    cardExpiryDateIncomplete: 'Expiry date incomplete',
    cardExpiryDateInvalid: 'Invalid expiry date',
    cardSecurityCodeInvalid: 'Invalid security code',
    emailFormatInvalid: 'Invalid email format',
    insufficientCharacters: 'Insufficient characters',
    
    // Payment errors
    paymentDeclinedTryAgain: 'Payment declined. Please try again.',
    paymentDeclinedNotEnoughFunds: 'Insufficient funds',
    paymentDeclinedInvalidCustomerData: 'Invalid payment details',
    paymentDeclinedInvalidPaymentSessionData: 'Invalid payment session data',
    paymentDeclinedMerchantMisconfiguration: 'Merchant configuration error',
    
    // Search and selection
    noMatchesFound: 'No matches found',
    trySearchingWithAnotherTerm: 'Try searching with another term',
    
    // Miscellaneous
    optional: 'Optional',
    useShippingAsBilling: 'Use shipping address as billing address',
    preferredSchemeCta: 'Select Preferred Scheme',
    preferredSchemeDescription: 'Please select your preferred card scheme'
  }
};
```

## 📡 Event Handling

### 📋 Complete Event Reference

```typescript
const eventHandlers = {
  // Component lifecycle
  onReady: (paymentMethod: string) => {
    console.log('Component ready for:', paymentMethod);
  },
  
  // Form validation
  onChange: (paymentMethod: string, isValid: boolean, isAvailable: boolean) => {
    console.log('Form state changed:', { paymentMethod, isValid, isAvailable });
    // Update UI based on form validity
  },
  
  // Payment submission
  onSubmit: (paymentMethod: string) => {
    console.log('Payment submitted for:', paymentMethod);
    // Show loading state
  },
  
  // Tokenization
  onTokenized: async (tokenizationResult: TokenizationResult) => {
    console.log('Card tokenized:', tokenizationResult);
    
    if (tokenizeResult.data.card_type === 'CREDIT') {
      return {
        continue: false,
        error: 'Credit cards are not accepted.',
      };
    }

    await utilizeToken(tokenizationResult.token);

    return { continue: true };
  },
  
  // Payment success
  onSuccess: (paymentMethod: string, paymentID: string) => {
    console.log('Payment successful:', { paymentMethod, paymentID });
    // Navigate to success screen
  },
  
  // Error handling
  onError: (error: Error) => {
    console.error('Payment error:', error);
    // Show error message to user
  },
  
  // Handle Submit
  const handleSubmit = async (sessionData: string) => {

    // Provide the `submitData` to your server-side integration for payment submission
    const submitResponse = await performPaymentSubmission(submitData);

    const apiCallResult: APICallResult = {
      success: true,
      paymentSessionSubmissionResult: submitResponse,
    };

    return apiCallResult;
  }
};

<CheckoutProvider {...eventHandlers}>
  {/* Components */}
</CheckoutProvider>
```

## 🔍 Linting & formatting

This repository uses [Biome](https://biomejs.dev/) for linting and formatting (replacing ESLint and Prettier). Configuration lives in the root `biome.json`. Each sample app under `examples/` includes a `biome.json` that extends the root config (`root: false`).

### Prerequisites

1. Clone the repository and install dependencies from the package root:

   ```sh
   yarn install
   ```

2. Ensure you are using the Node.js version required by the project (see sample `package.json` `engines` fields where present).

### Root package scripts (library + all workspaces)

Run these from the repository root (`checkout-react-native-components/`):

| Script | Command | Purpose |
|--------|---------|---------|
| **CI check (lint + format)** | `yarn check:ci` | Same as CI: runs `biome ci .` — fails if sources need formatting or violate lint rules. Use this before pushing. |
| **Lint only** | `yarn lint` | Runs `biome lint .` (lint diagnostics without applying changes). |
| **Format (write)** | `yarn format` | Runs `biome format --write .` to apply formatting. |
| **Format (verify)** | `yarn format:check` | Runs `biome format .` to verify formatting without writing files. |
| **Check (lint + format + assist)** | `yarn check` | Runs `biome check .` (full check, no writes). |
| **Auto-fix** | `yarn lint:fix` | Runs `biome check --write .` to apply safe fixes and formatting. |

Some fixes are marked **unsafe** by Biome (for example certain type-related suggestions). Apply them only when you intend to:

```sh
yarn biome check --write --unsafe .
```

### Sample apps (`examples/*`)

From the root, you can run Biome for a specific workspace:

```sh
yarn workspace CheckoutRnNewArch lint
yarn workspace checkoutexponewarch format
yarn workspace shared-components lint
```

Or `cd` into an example and use the same script names (`lint`, `format`, `lint:fix`) defined in that package’s `package.json`.

### CI

The GitHub Actions workflow runs `yarn check:ci` in the lint job so that pull requests must satisfy Biome’s formatter and linter.

### References

- [Biome getting started](https://biomejs.dev/guides/getting-started/)
- [Configuration reference](https://biomejs.dev/reference/configuration/)

## 🪝 Git hooks (Lefthook)

This repository uses [Lefthook](https://lefthook.dev/) so common checks run locally and stay aligned with the main CI jobs (`yarn check:ci`, `yarn typecheck`, `yarn test`). Configuration lives in the root [`lefthook.yml`](lefthook.yml). Native example builds, `yarn prepare` (bob build), and full Jest with coverage are intentionally left to GitHub Actions.

### Installing hooks

After you clone or pull, run `yarn install` from the repository root. The `postinstall` script runs `lefthook install` when `lefthook.yml` is present (this only applies when you work from a git checkout of this repo; the published npm package does not ship `lefthook.yml`). You can also run `yarn lefthook install` manually if needed.

### What runs when

| Hook | Command(s) |
|------|------------|
| **pre-commit** | `yarn biome check --staged` (lint + format check on staged files only) |
| **pre-push** (parallel) | `yarn typecheck` and `yarn test --onlyChanged --maxWorkers=2` |
| **commit-msg** | `yarn commitlint --edit` (conventional commits; see `commitlint` in `package.json`) |
| **post-checkout** | `yarn install --immutable` |

All of the above are **skipped when the `CI` environment variable is set** (for example in many CI systems). If your local terminal sets `CI=1`, hooks will not run until you unset it.

### Bypassing hooks

- Disable Lefthook for a single Git invocation: `LEFTHOOK=0 git commit` (or `git push`).
- Skip Git’s hooks entirely when you must: `git commit --no-verify` / `git push --no-verify`.

### Manual runs

```sh
yarn lefthook run pre-commit
yarn lefthook run pre-push
yarn lefthook validate
```

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

