> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lite.sa/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter SDK

> How to integrate and manage payments through the lite Flutter SDK.

Accept card payments, and Apple Pay on iOS, with lite's Payment Sheet, an embeddable payment form, or individual card fields in your own layout.

## Overview

The lite Flutter SDK lets customers pay without leaving your app. It provides three integration options:

* Payment Sheet: a complete checkout presented over your existing interface.
* Embeddable payment form: the same lite-managed checkout UI placed inside your Flutter layout.
* Individual card fields: secure card number, expiry, CVV, and cardholder name inputs that you place in your own layout. You provide the pay button.

The SDK collects and validates card details, encrypts them on the device, tokenizes the payment method, and submits the payment to lite. Raw card details are not sent to your server. Card fields stay in the native iOS and Android SDKs. Your Dart code receives a payment result, never the card number.

Use the Payment Sheet for the quickest integration, or the embeddable form when you need control over where checkout appears inside your screen. Use individual card fields when the checkout UI is yours. See [Individual card fields](#4-individual-card-fields).

| Feature | iOS | Android |
| - | - | - |
| Card payments | Yes | Yes |
| Saved cards | Yes | Yes |
| 3D Secure | Yes, handled by the SDK | Yes, handled by the SDK |
| Apple Pay | Yes | No |
| Payment Sheet | Yes | Yes |
| Embeddable payment form | Yes | Yes |
| Individual card fields | Yes | Yes |

## How it works

1. Your server creates a checkout session with the lite API and receives a session secret.
2. Your app gives the session secret to the SDK.
3. The SDK loads the session and displays the payment methods enabled for it.
4. The customer enters a new card or selects a saved card.
5. The SDK encrypts new card data, tokenizes it, and submits the payment.
6. If required, the SDK presents and completes 3D Secure automatically.
7. Your server confirms the final payment result before fulfilling the order.

## Before you begin

You need:

* A lite account with a configured channel.
* Valid lite API keys on your server.
* A backend endpoint that creates lite checkout sessions.
* Flutter 3.44.2 or later, and a Dart SDK compatible with `^3.12.2`.
* For iOS: Xcode 15 or later and an app targeting iOS 15 or later.
* For Android: an app with `minSdk` 26 or later, `compileSdk` 35 or later, and Java 17.

<Warning>
  Never place lite API keys in your Flutter app. Create checkout sessions on your server and send only the session secret to the app. Do not log the session secret.
</Warning>

## Integrate the SDK

### 1. Install the SDK

Add the plugin to your app:

```yaml theme={null}
dependencies:
  lite_flutter: ^0.0.2
```

Then run `flutter pub get`.

Install this plugin only. It brings in the native SDKs itself: `sa.lite:lite-android` from Maven Central, and `LiteSDK` from the `lite-sa/lite-ios` Swift package. Do not add those packages to your app again.

<Note>
  On iOS the SDK is distributed through Swift Package Manager only. CocoaPods is not supported.
</Note>

<Tabs>
  <Tab title="iOS">
    Flutter 3.44 enables Swift Package Manager by default, and nothing goes in your `Podfile`. If the app turned Swift Package Manager off, remove `enable-swift-package-manager: false` from `flutter: config:` in `pubspec.yaml` and run `flutter pub get` again.
  </Tab>

  <Tab title="Android">
    Maven Central is already on the plugin's repository list, so the app does not need a `settings.gradle` change for lite. Set `minSdk` to 26 or higher. Make the host activity a `FlutterFragmentActivity`. The embeddable form and individual card fields require it, and the Payment Sheet should use the same activity:

    ```kotlin theme={null}
    import io.flutter.embedding.android.FlutterFragmentActivity

    class MainActivity : FlutterFragmentActivity()
    ```

    The native Android SDK declares the internet permission, and it is merged into the app. Check the merged manifest if the build removes library permissions.
  </Tab>
</Tabs>

### 2. Create a checkout session

Your server must create a checkout session using your lite API credentials and return its session secret to the app.

<Warning>
  Do not generate the session secret in the app and do not include your lite API keys in the application bundle, Dart code, or Android `BuildConfig`. Anything shipped in the app can be extracted, so a server API key placed there is exposed to your customers and lets anyone impersonate your account. If a private server key has ever shipped inside an app build, rotate it immediately.
</Warning>

```bash theme={null}
curl https://api.lite.sa/checkout-sessions \
  -H "x-api-key: $LITE_API_KEY" \
  -H "x-idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "<active channel_id>",
    "amount": 4999,
    "currency": "SAR",
    "customer": {
      "email": "john.doe@email.com",
      "first_name": "John",
      "last_name": "Doe"
    },
    "order": { "reference": "ORD-123456" },
    "redirect_urls": {
      "success": "https://example.com/return?session_id={SESSION_ID}",
      "failure": "https://example.com/checkout"
    },
    "expiry": 1800
  }'
```

<Accordion title="Response">
  ```json theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "client_secret": "cs_a1b2c3d4e5f6",
    "status": "Pending",
    "expires_on": "2026-06-01T12:30:00.000Z",
    "order_id": "ORD-123456",
    "amount": 4999,
    "currency": "SAR"
  }
  ```
</Accordion>

Return `client_secret` to your app, where the SDK takes it as `clientSecret`. Keep the session `id` on your server so you can confirm the payment afterwards.

The session determines the amount, currency, enabled payment methods, available card networks, and eligible saved cards. Create a new session for each checkout attempt.

### 3. Modes of integration

The Payment Sheet and the embeddable form take the session secret and deliver a `PayResult` when the payment attempt finishes. Pick the one that fits your checkout screen. For a checkout you draw yourself, use [Individual card fields](#4-individual-card-fields).

<Tabs>
  <Tab title="Payment Sheet">
    The Payment Sheet is the recommended integration. `present()` returns a future that completes with a `PayResult` carrying the payment status once the sheet is consumed:

    ```dart theme={null}
    import 'package:flutter/services.dart';
    import 'package:lite_flutter/lite_flutter.dart';

    Future<void> presentCheckout(String sessionSecret) async {
      try {
        final PayResult result = await LitePaymentSheet.present(sessionSecret);
        handlePaymentResult(result);
      } on PlatformException catch (error) {
        if (error.code == 'already_presenting') {
          // A sheet is already on screen.
          return;
        }
        // Show a customer-safe message and allow a retry when appropriate.
      }
    }
    ```

    The sheet shows the payment result until the customer dismisses it.

    <Note>
      Only one Payment Sheet can be presented at a time. A second call throws a `PlatformException` with the code `already_presenting`.
    </Note>
  </Tab>

  <Tab title="Embeddable form">
    Use `LitePaymentForm` when checkout must appear inside your own screen. Give it a bounded height. The native form scrolls inside that space, so keep it out of a parent `ListView` or other scrollable.

    On Android the host activity must be a `FlutterFragmentActivity`, as shown in the install step. A plain `FlutterActivity` cannot host the form.

    ```dart theme={null}
    import 'package:flutter/material.dart';
    import 'package:lite_flutter/lite_flutter.dart';

    class CheckoutPage extends StatelessWidget {
      const CheckoutPage({super.key, required this.sessionSecret});

      final String sessionSecret;

      @override
      Widget build(BuildContext context) {
        return LitePaymentForm(
          key: ValueKey<String>(sessionSecret),
          clientSecret: sessionSecret,
          onPayResult: handlePaymentResult,
        );
      }
    }

    void handlePaymentResult(PayResult result) {
      // Handle every result status and confirm the payment from your server.
    }
    ```

    Passing `clientSecret` starts the SDK automatically. Key the widget on the session secret so a new session mounts a new form.

    The result is delivered only through `onPayResult`. The form does not render a result screen.

    <Warning>
      Keep this widget in the tree until `onPayResult` runs. Removing it disposes the native checkout.
    </Warning>
  </Tab>
</Tabs>

The Payment Sheet and the embeddable form accept the same `LitePaymentConfiguration`. Its only setting, `cardLayout`, selects how the card fields are laid out:

| Layout | Behavior |
| - | - |
| `LiteCardLayout.compact` | A grouped, compact card-information form. This is the default. |
| `LiteCardLayout.form` | Separate card number, expiry, CVV, and optional cardholder fields. |
| `LiteCardLayout.line` | A condensed single-row card entry. |

### 4. Individual card fields

Use individual card fields when you draw the checkout yourself. You place `LiteCardNumberField`, `LiteExpiryField`, `LiteCvvField`, and `LiteCardholderNameField`, and you provide the pay button. The SDK opens the session and hosts those fields in the native iOS and Android SDKs. It encrypts a new card, tokenizes it, and presents 3D Secure when required.

Your Dart code receives a `LiteCardFieldChange` and a `PayResult`. The change describes the field. It does not include the card number, expiry, CVV, or cardholder name.

On Android the host activity must be a `FlutterFragmentActivity`, the same requirement as the embeddable form.

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:lite_flutter/lite_flutter.dart';

class CardCheckout extends StatefulWidget {
  const CardCheckout({super.key, required this.sessionSecret});

  final String sessionSecret;

  @override
  State<CardCheckout> createState() => _CardCheckoutState();
}

class _CardCheckoutState extends State<CardCheckout> {
  late final LiteCheckout _checkout = LiteCheckout(clientSecret: widget.sessionSecret);
  bool _saveCard = false;

  @override
  void initState() {
    super.initState();
    _checkout.addListener(_rebuild);
    _checkout.onPayResult = handlePaymentResult;
    _open();
  }

  Future<void> _open() async {
    try {
      await _checkout.start();
    } on LiteCheckoutException catch (error) {
      // error.message says the secret was rejected. Call start() again to retry.
    } on StateError {
      // The checkout was disposed before the session loaded.
    }
  }

  @override
  void dispose() {
    _checkout.removeListener(_rebuild);
    _checkout.dispose();
    super.dispose();
  }

  void _rebuild() {
    if (mounted) setState(() {});
  }

  void _onFieldChange(LiteCardFieldChange change) {
    // Each field reports its own change. The payload is change.field, change.valid,
    // change.brand, change.error, change.focused, and change.message.
  }

  VoidCallback? _payPressed() {
    if (!_checkout.isReadyToPay) return null;
    return _pay;
  }

  Future<void> _pay() async {
    await _checkout.pay(storeForFuture: _saveCard);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        LiteCardNumberField(checkout: _checkout, onChange: _onFieldChange),
        LiteExpiryField(checkout: _checkout, onChange: _onFieldChange),
        LiteCvvField(checkout: _checkout, onChange: _onFieldChange),
        LiteCardholderNameField(checkout: _checkout, onChange: _onFieldChange),
        Row(
          children: <Widget>[
            const Expanded(child: Text('Save card for future purchase')),
            Switch(
              value: _saveCard,
              onChanged: (bool value) {
                setState(() {
                  _saveCard = value;
                });
              },
            ),
          ],
        ),
        FilledButton(
          onPressed: _payPressed(),
          child: const Text('Pay'),
        ),
      ],
    );
  }
}
```

Pass the session secret to the `LiteCheckout` constructor, then await `start()`. It completes when the session is ready. An expired or rejected secret throws `LiteCheckoutException`, and a later `start()` retries. The fields can sit in the tree before `start()` finishes, and they stay empty until it succeeds. Keep that `LiteCheckout` alive until the fields leave the tree, then call `dispose()`. Disposal clears the collected card data.

`onPayResult` receives the result of `pay()`, including Apple Pay. The pay button awaits `pay()` and does not handle that result again.

Mount at most one field of each type on a checkout. A second field of the same type is not shown. The fields stay alive when a `ListView` scrolls them off screen, so the typed card is not cleared.

`onChange` reports:

| Field | Meaning |
| - | - |
| `valid` | The field's current value passes format checks. An empty field is not valid, except the cardholder name, which is optional. |
| `brand` | `LiteCardBrand.visa`, `LiteCardBrand.mastercard`, `LiteCardBrand.mada`, or null when the brand is unknown. Set from the card number field. |
| `error` | `LiteFieldError.invalidNumber`, `LiteFieldError.invalidExpiry`, or `LiteFieldError.invalidCvv`. Null when the field is empty or valid. |
| `focused` | Whether this field has focus. |
| `message` | Text for `error`. The field does not draw it. Show it beside the field when you want an inline error. |

`LiteFieldStyle` is optional. It styles the native field text, placeholder, border, and colors. It does not style your pay button, and it does not draw `message`. Pass the same style to each field you want to match. Any property you omit keeps the default shown here. Sizes are logical pixels.

```dart theme={null}
const LiteFieldStyle fieldStyle = LiteFieldStyle(
  textColor: Color(0xFF161616),
  placeholderColor: Color(0xFFA2A2A2),
  backgroundColor: Color(0xFFFFFFFF),
  borderColor: Color(0xFFDEDEDE),
  // Border only. The text stays textColor, and message is not drawn.
  errorColor: Color(0xFFCD0202),
  // Border only, while the field is focused and has no error.
  focusedBorderColor: Color(0xFF6335EA),
  // 0 removes the border, so errorColor and focusedBorderColor are not drawn.
  borderWidth: 1,
  cornerRadius: 8,
  // Left and right inset on both iOS and Android.
  horizontalPadding: 12,
  // Top and bottom inset on both iOS and Android.
  verticalPadding: 0,
  // Shortest height. Default 48. The field grows with the system text size
  // and will not draw shorter than 48.
  minHeight: 48,
  // Scales with the system text size.
  fontSize: 17,
);

LiteCardNumberField(
  checkout: checkout,
  style: fieldStyle,
  onChange: onFieldChange,
)
```

`isReadyToPay` is true when a new card is complete, or an enabled saved card is selected, and pay is not locked. `pay()` authorizes that selection and returns a `PayResult`. Pass `storeForFuture: true` to save a new card for a later checkout. That flag does nothing when a saved card is already selected. The fields do not draw the save switch. You draw it and pass its value. Handle the result with the same statuses as the Payment Sheet and embeddable form, and confirm the payment on your server before fulfilling the order.

`LiteCheckout` notifies listeners when the session updates. After `start()`:

* `instruments` lists the saved cards on the session. `title` and `expiry` are safe to display. `getStoredInstruments(enabledOnly: true)` returns only cards the session can charge.
* `selectStoredInstrument` selects an enabled saved card and returns false when the id is missing or disabled.
* `selectNewCard` returns the next `pay()` to a newly entered card.

`LiteApplePayButton` draws the native Apple Pay button on iOS when Apple Pay is available for the session. It takes no space on Android. Set `onPayResult` on the checkout to receive its result. Apple Pay setup is the same as for the Payment Sheet. Follow [Enable Apple Pay](#enable-apple-pay).

A card number that is format-valid stays valid even when that scheme is turned off for the session. `pay()` then returns `failure` with `errorCode` `PAYMENT_METHOD_NOT_SUPPORTED`. `isPayLocked` becomes true and `payLockMessage` is the text to show above your button. Leave the customer on your form. Editing the card number clears the lock.

A dismissed 3D Secure challenge returns `cancelled` with `errorCode` `THREE_DS_CHALLENGE_DISMISSED`. Leave the customer on your form.

Keep the checkout and its fields mounted until `pay()` finishes. Removing them disposes the native checkout.

### 5. Save and reuse cards

When the session contains eligible saved cards, the Payment Sheet and embeddable form display them automatically. The customer can select a saved card or enter a new one. Individual card fields do not draw that list. Read `instruments` on the checkout and call `selectStoredInstrument` or `selectNewCard`.

For a new card, the Payment Sheet and embeddable form include a Save card for future purchase control. Individual card fields do not. Call `pay(storeForFuture: true)` when your own switch is on. Saved cards are available only when the customer and checkout session are eligible for them. Your server remains responsible for creating the session with the correct customer context.

### 6. Handle the result

The sheet future, the form callback, and `LiteCheckout.onPayResult` all receive a `PayResult`:

```dart theme={null}
class PayResult {
  final LitePaymentStatus status;
  final String? paymentId;
  final String? error;
  final String? errorCode;
}
```

```dart theme={null}
void handlePaymentResult(PayResult result) {
  switch (result.status) {
    case LitePaymentStatus.success:
      // Show success, then confirm result.paymentId from your server.
      break;
    case LitePaymentStatus.failure:
      // Show result.error and allow the customer to retry when appropriate.
      break;
    case LitePaymentStatus.processing:
      // Keep the order pending and confirm the result from your server.
      break;
    case LitePaymentStatus.cancelled:
      // The customer dismissed checkout or cancelled authentication.
      break;
    case LitePaymentStatus.alreadyCompleted:
      // This session had already completed. Confirm it from your server.
      break;
  }
}
```

| Status | Meaning |
| - | - |
| `success` | The payment was authorized or captured. `paymentId` identifies the payment. |
| `failure` | The payment failed or was rejected. Display `error` and use `errorCode` for programmatic handling. |
| `processing` | The payment has not reached a final state. Keep the order pending and check it from your server. |
| `cancelled` | The customer dismissed checkout or cancelled authentication. |
| `alreadyCompleted` | The opened session already had a completed payment. This is not a new payment attempt. |

The built-in form disables its pay button while a payment is in progress. Keep the checkout screen mounted until the result arrives.

### 7. Confirm the payment on your server

<Warning>
  A client-side success result is not sufficient for order fulfillment. The app can close, lose connectivity, or be modified.
</Warning>

Always retrieve the checkout session or payment from your server and verify its final state before fulfilling the order.

## Enable Apple Pay

Apple Pay is available on iOS when the checkout session enables it. Android does not show Apple Pay.

The plugin presents Apple Pay through the native iOS SDK, so the setup is the same as for a native iOS app. Follow [Enable Apple Pay in the iOS SDK guide](/guides/iOS-sdk#enable-apple-pay) and apply the Xcode steps to the `Runner` target of your Flutter project.

## Clean up

The Payment Sheet clears card fields when it closes.

For an embeddable integration, remove `LitePaymentForm` from the tree when the customer abandons checkout. Disposal clears the collected card data and detaches the native checkout. To start again with a new session, mount a new `LitePaymentForm` with a new `ValueKey`.

For individual card fields, call `dispose()` on the `LiteCheckout` when the customer leaves. That clears the collected card data and detaches the native session.

## Integration checklist before going live

Run through this checklist in the lite sandbox environment with a sandbox API key before you switch to live keys. See [Sandbox testing](/guides/lite-sandbox-testing-guide) for enabling test mode, creating the key, and configuring a channel, and [Test cards](/guides/test-cards) for the cards to use.

* Successful card payments.
* Declined and unsupported cards.
* 3D Secure success, failure, and cancellation.
* Processing payments.
* Saved-card display and payment.
* Saving a new card for future use.
* Payment Sheet dismissal.
* A second sheet presentation while one is already open.
* Replacing the embeddable form when the session secret changes.
* Rotation, with the Android host as a `FlutterFragmentActivity`.
* Reopening a completed session.
* Apple Pay on a correctly provisioned physical iOS device.
* Individual card fields: a completed new card, a scheme the session does not accept, and a dismissed 3D Secure challenge.

Test that your server confirms the final payment state before your system fulfills an order.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.