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

# SDK Flutter (Mobile)

> Nhúng luồng Danh sách công việc và ký số eKYC vào ứng dụng Flutter bằng plugin econtract_task_module

## Tổng quan

`econtract_task_module` là **Flutter plugin** cho phép nhúng toàn bộ luồng Danh sách công việc của econtractid vào ứng dụng Flutter của bạn bằng **một hàm duy nhất** — chạy trên `baseUrl` và `accessToken` của bạn.

<CardGroup cols={2}>
  <Card title="Một hàm duy nhất" icon="bolt">
    `openTaskList()` mở thẳng luồng: danh sách → chi tiết → xem PDF → ký số eKYC → từ chối
  </Card>

  <Card title="Native đóng gói sẵn" icon="box">
    Bridge eKYC (Android AAR + iOS xcframework) đi kèm và tự đăng ký
  </Card>

  <Card title="Chạy trên hạ tầng của bạn" icon="key">
    Truyền `baseUrl` + token lúc chạy; token giữ in-memory, không đụng dữ liệu app host
  </Card>

  <Card title="Thoát về host" icon="door-open">
    signOut / back / 401 đều quay về app của bạn kèm callback — không hiện màn login riêng
  </Card>
</CardGroup>

### Kiến trúc

Plugin gồm hai phần:

* **Dart (`lib/`)** — luồng Task List và API công khai `EContractModule`
* **Native bridge eKYC** — `android/` (SDK qua Maven repo bundled) và `ios/` (`EContractIDSDK.xcframework` vendored), tự đăng ký qua `GeneratedPluginRegistrant`

### Yêu cầu

| Thành phần | Phiên bản                                |
| ---------- | ---------------------------------------- |
| Flutter    | 3.29.2 (khuyến nghị dùng FVM)            |
| Dart SDK   | `>=3.3.1 <4.0.0`                         |
| Android    | minSdk 24, Hilt, core library desugaring |
| iOS        | Deployment target ≥ 13                   |

<Warning>
  **Bắt buộc cấu hình native một lần.** Vì luồng có ký số/eKYC dùng SDK native, app host phải cấu hình ở tầng nền tảng (Hilt, quyền camera/NFC). Đây là ràng buộc của hệ điều hành và SDK — không plugin nào thay thế được.
</Warning>

<Info>
  **Không cần Firebase.** Module đã loại bỏ Firebase — host không cần `google-services.json` hay `GoogleService-Info.plist`.
</Info>

***

## Cài đặt

### 1. Thêm dependency

Trong `pubspec.yaml` của app host (phân phối qua private git repo):

```yaml theme={null}
dependencies:
  econtract_task_module:
    git:
      url: https://github.com/hoanghung19/econtract_task_module.git
      ref: main
```

<Tip>
  Khi phát triển tại máy, dùng path dependency:

  ```yaml theme={null}
  dependencies:
    econtract_task_module:
      path: ../econtract_task_module
  ```
</Tip>

### 2. Lấy package

```bash theme={null}
# FVM (khuyến nghị)
fvm flutter pub get

# hoặc
flutter pub get
```

<Info>
  Đây là **plugin**, không phải package thường. Native eKYC được kéo theo và tự đăng ký khi bạn thêm dependency — không cần copy bridge hay sửa `MainActivity`/`AppDelegate` để add plugin.
</Info>

<Warning>
  Thêm dependency xong vẫn **chưa build được** — phải cấu hình host Android/iOS ở phần Tích hợp bên dưới.
</Warning>

***

## Tích hợp

### Gọi module

Ở bất kỳ đâu có `BuildContext`:

```dart theme={null}
import 'package:econtract_task_module/econtract_module.dart';

FilledButton(
  onPressed: () {
    EContractModule.openTaskList(
      context,
      baseUrl: 'https://api.econtractid.com/api',
      accessToken: '<access token của bạn>',
      onExit: () {
        // Người dùng thoát luồng → quay lại app host
      },
      onSessionExpired: () {
        // Gặp 401 → lấy token mới rồi gọi updateAccessToken
      },
    );
  },
  child: const Text('Mở Danh sách công việc'),
);
```

`openTaskList` tự khởi tạo (Hive + DI + eKYC) một lần, **không gọi `runApp`**, và đẩy route chứa toàn bộ luồng lên navigator của host.

### Cấu hình host — Android

eKYC SDK dùng Hilt và camera/NFC. Các mục sau phải nằm ở **app host**.

**a. `android/settings.gradle`**

```groovy theme={null}
plugins {
    // ... các plugin sẵn có
    id "org.jetbrains.kotlin.android" version "2.1.0" apply false
    id "com.google.dagger.hilt.android" version "2.56.2" apply false
    id "com.google.devtools.ksp" version "2.1.0-1.0.29" apply false
}
```

**b. `android/app/build.gradle`**

```groovy theme={null}
plugins {
    id "com.android.application"
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
    id "com.google.dagger.hilt.android"      // ← thêm
    id "com.google.devtools.ksp"             // ← thêm
}

android {
    compileSdk 36
    defaultConfig {
        minSdkVersion 24                      // ← eKYC SDK yêu cầu ≥ 24
    }
    compileOptions {
        coreLibraryDesugaringEnabled true     // ← bắt buộc
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
    implementation 'com.google.dagger:hilt-android:2.56.2'
    ksp 'com.google.dagger:hilt-android-compiler:2.56.2'
    // KHÔNG khai báo eKYC SDK — đã đến từ plugin
}
```

**c. Application class `@HiltAndroidApp`**

```kotlin theme={null}
package com.khachhang.app

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App : Application()
```

**d. MainActivity kế thừa `FlutterFragmentActivity`**

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

class MainActivity : FlutterFragmentActivity()
```

<Tip>
  **Không** cần `flutterEngine.plugins.add(...)` — plugin tự đăng ký.
</Tip>

**e. `AndroidManifest.xml`**

```xml theme={null}
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />

<application android:name=".App" ...>   <!-- ← trỏ tới class @HiltAndroidApp -->
```

### Cấu hình host — iOS

**a. Deployment target ≥ 13**

Trong `ios/Podfile`: `platform :ios, '13.0'` (hoặc cao hơn), và đặt cùng giá trị `IPHONEOS_DEPLOYMENT_TARGET` trong Xcode.

**b. `ios/Runner/Info.plist`**

```xml theme={null}
<key>NSCameraUsageDescription</key>
<string>Cần camera để xác thực eKYC</string>
<key>NFCReaderUsageDescription</key>
<string>Cần NFC để đọc chip trên CCCD</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>A0000002471001</string>
</array>
```

**c. Entitlements NFC**

Xcode → target **Runner** → **Signing & Capabilities** → thêm **Near Field Communication Tag Reading**.

<Info>
  `pod install` tự kéo plugin và `EContractIDSDK.xcframework` (vendored). Plugin tự đăng ký — **không** sửa `AppDelegate`.
</Info>

### Hành vi thoát luồng

| Tình huống               | Kết quả                               |
| ------------------------ | ------------------------------------- |
| Người dùng bấm signOut   | Quay về host + gọi `onExit`           |
| Back ở màn gốc Task List | Quay về host + gọi `onExit`           |
| API trả `401`            | Gọi `onSessionExpired` + quay về host |

Module **không hiển thị màn login của riêng nó** — đăng nhập và làm mới token do app host lo.

***

## Luồng token

Module không tự đăng nhập. Backend của bạn cấp token qua [`partner-login`](/api-reference/xac-thuc):

```mermaid theme={null}
sequenceDiagram
    participant A as App Flutter
    participant P as Backend của bạn
    participant E as econtractid API

    P->>E: POST /auth/partner-login (x-api-key)
    E-->>P: accessToken
    P-->>A: accessToken
    A->>A: openTaskList(baseUrl, accessToken)
    Note over A,E: SDK gắn header x-access-token cho mọi request
    E-->>A: 401 (token hết hạn)
    A->>A: onSessionExpired
    A->>P: Xin token mới
    P->>E: POST /auth/partner-login
    E-->>P: accessToken mới
    P-->>A: accessToken mới
    A->>A: EContractModule.updateAccessToken(newToken)
```

<Warning>
  **API Key không được đưa vào app di động.** Ứng dụng đã cài đặt trên máy người dùng là môi trường không tin cậy — bất kỳ khoá nào nhúng trong app đều có thể bị trích xuất. Luôn để backend của bạn gọi `partner-login` rồi trả token về cho app.
</Warning>

<Note>
  Trên production, access token có hiệu lực **15 ngày** và hệ thống **không có endpoint refresh** (môi trường development cấu hình dài hơn nhiều). Cách xử lý duy nhất khi hết hạn là backend gọi lại `partner-login`. Xem [Vòng đời token](/api-reference/xac-thuc).
</Note>

***

## API Reference

Lớp `EContractModule` là **điểm vào công khai duy nhất**. Import `package:econtract_task_module/econtract_module.dart`.

| Hàm                        | Mô tả                                                         |
| -------------------------- | ------------------------------------------------------------- |
| `openTaskList(...)`        | Mở luồng Task List trên `baseUrl` + token của host            |
| `ensureInitialized()`      | Khởi tạo Hive + DI + eKYC (idempotent). `openTaskList` tự gọi |
| `updateAccessToken(token)` | Cập nhật token giữa phiên                                     |

### `openTaskList`

```dart theme={null}
static Future<void> openTaskList(
  BuildContext context, {
  required String baseUrl,
  required String accessToken,
  VoidCallback? onExit,
  VoidCallback? onSessionExpired,
});
```

| Tham số            | Kiểu                | Mô tả                                             |
| ------------------ | ------------------- | ------------------------------------------------- |
| `context`          | `BuildContext`      | Context của host để push route module             |
| `baseUrl`          | `String` (bắt buộc) | Base URL API, áp cho mọi request trong phiên      |
| `accessToken`      | `String` (bắt buộc) | Token; giữ in-memory, gắn header `x-access-token` |
| `onExit`           | `VoidCallback?`     | Gọi khi thoát luồng (signOut / back ở màn gốc)    |
| `onSessionExpired` | `VoidCallback?`     | Gọi khi gặp `401`, trước khi quay về host         |

### `ensureInitialized`

```dart theme={null}
static Future<void> ensureInitialized();
```

Khởi tạo idempotent — gọi nhiều lần chỉ chạy một lần, mọi caller cùng await một future. Thường không cần gọi trực tiếp.

### `updateAccessToken`

```dart theme={null}
static void updateAccessToken(String token);

// ví dụ khi host lấy được token mới
EContractModule.updateAccessToken(newToken);
```

***

## Khắc phục sự cố

| Triệu chứng                                                    | Nguyên nhân / cách xử lý                                                                          |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Build lỗi liên quan `Hilt_EKycSdkActivity` / `@HiltAndroidApp` | Thiếu Application `@HiltAndroidApp` hoặc chưa áp plugin Hilt/KSP                                  |
| Build lỗi minSdk                                               | Nâng `minSdkVersion` ≥ 24                                                                         |
| Crash khi mở camera / `ClassCastException … FragmentActivity`  | `MainActivity` phải kế thừa `FlutterFragmentActivity`                                             |
| Lỗi desugaring / Java 8 API                                    | Bật `coreLibraryDesugaringEnabled true` + thêm `desugar_jdk_libs`                                 |
| iOS crash khi mở camera/NFC                                    | Thiếu usage string trong `Info.plist` hoặc entitlements NFC                                       |
| iOS: `Undeclared identifier 'EKycPlugin'`                      | Chạy lại `pod install`; đảm bảo dùng bản plugin mới                                               |
| `Default FirebaseApp is not initialized`                       | Không đến từ module (đã bỏ Firebase). Kiểm tra code Firebase của chính host                       |
| Ảnh empty-state không hiển thị                                 | Lỗi hiển thị thuần tuý — assets nằm trong package; nếu cần, cấu hình `package:` cho `flutter_gen` |

<Tip>
  Thư mục `example/` trong repo plugin là app host demo đã build được cả Android lẫn iOS — dùng làm tham chiếu cấu hình chuẩn.
</Tip>

***

## Câu hỏi thường gặp

<AccordionGroup>
  <Accordion title="Vì sao code native nằm trong module?">
    Module là Flutter **plugin** (không phải package thường). Nhờ đó thư mục `android/` + `ios/` chứa bridge eKYC được biên dịch vào app host và tự đăng ký — bạn chỉ cần thêm 1 dependency.
  </Accordion>

  <Accordion title="Có cần Firebase không?">
    Không. Module đã loại `firebase_core`/`firebase_remote_config` và local notification. Host không cần `google-services.json` / `GoogleService-Info.plist`.
  </Accordion>

  <Accordion title="Vì sao Android phải có Hilt?">
    eKYC SDK dùng Hilt nội bộ — `EKycSdkActivity` cần `@HiltAndroidApp` trên Application của host. Đây là ràng buộc của SDK, plugin không thể tự thêm Application class.
  </Accordion>

  <Accordion title="Token có bị ghi xuống máy không?">
    Token truyền vào module được giữ **in-memory** theo phiên, gắn vào header mỗi request, và xoá khi thoát luồng. Module không ghi token xuống thiết bị và không đụng tới dữ liệu lưu trữ của app host.
  </Accordion>

  <Accordion title="baseUrl áp cho những request nào?">
    Toàn bộ request trong phiên của module (qua interceptor) và cả đường tải PDF riêng. Khi thoát luồng, mọi thứ trở về mặc định.
  </Accordion>

  <Accordion title="Có bắt buộc tích hợp cả Android lẫn iOS?">
    Không — tích hợp nền tảng nào thì cấu hình host nền tảng đó. Cả hai đều đã được kiểm chứng build trong `example/`.
  </Accordion>

  <Accordion title="Có giấu được source Dart không?">
    Không. Flutter package/plugin phân phối dưới dạng source Dart. Private git repo chỉ giới hạn **ai được truy cập**, không giấu được nội dung source. Muốn giấu hoàn toàn cần host native + Flutter module đã biên dịch — ngoài phạm vi hiện tại.
  </Accordion>
</AccordionGroup>

***

## Checklist trước khi lên production

<Warning>
  Hai hạng mục sau **phải được xử lý** trước khi phát hành ứng dụng thật:

  * **Xác thực chứng chỉ SSL** — `dio_consumer.dart` hiện bỏ qua kiểm tra chứng chỉ SSL. Với `baseUrl` tuỳ ý, điều này khiến kết nối có thể bị chặn và đọc bởi bên thứ ba. Bỏ phần bypass và cân nhắc bật certificate pinning.
  * **Lưu trữ token phía host** — nếu app host lưu token trong Hive dạng văn bản thuần, hãy chuyển sang secure storage của nền tảng (Keychain / Keystore).
</Warning>

***

## Xem thêm

<CardGroup cols={2}>
  <Card title="Xác thực API" icon="key" href="/api-reference/xac-thuc">
    Lấy access token qua partner-login
  </Card>

  <Card title="Nhúng web (iframe)" icon="window-restore" href="/technical/nhung-iframe">
    Tích hợp cho nền tảng web
  </Card>
</CardGroup>
