vc-cdp-rn-sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,24 +10,35 @@ CDP SDK cho React Native — tracking events và user profiles cho hệ thống
10
10
  - [Khởi tạo](#khởi-tạo)
11
11
  - [Luồng hoạt động](#luồng-hoạt-động)
12
12
  - [User / Profile](#user--profile)
13
- - [Đăng nhập](#đăng-nhập-identifyuser)
14
- - [Cập nhật profile](#cập-nhật-profile-updateprofile)
15
- - [Lấy thông tin profile](#lấy-thông-tin-profile-getprofile)
16
- - [Trait setters](#trait-setters-chainable)
13
+ - [identifyUser](#identifyuser--đăng-nhập)
14
+ - [Trait Setters](#trait-setters--cập-nhật-từng-thuộc-tính)
15
+ - [updateProfile](#updateprofile--force-update)
16
+ - [getProfile](#getprofile--lấy-thông-tin-profile)
17
17
  - [Consents](#consents)
18
18
  - [Scores](#scores)
19
19
  - [Products](#products)
20
- - [Metadata](#metadata)
21
- - [Campaign / UTM](#campaign--utm)
22
- - [Push Token](#push-token)
23
- - [Đăng xuất](#đăng-xuất)
20
+ - [Metadata, Campaign, Push Token](#metadata-campaign-push-token)
21
+ - [getUserIdentities](#getuseridentities)
22
+ - [destroy_session](#destroy_session--đăng-xuất)
24
23
  - [Event Tracking](#event-tracking)
25
- - [Track sự kiện](#track-sự-kiện)
26
- - [Track màn hình](#track-màn-hình-screen)
27
- - [Flush thủ công](#flush-thủ-công)
28
- - [Lấy thông tin định danh](#lấy-thông-tin-định-danh)
24
+ - [track](#track--custom-event)
25
+ - [screen](#screen--màn-hình)
26
+ - [React Navigation](#tích-hợp-react-navigation)
27
+ - [Batch Processing](#batch-processing)
28
+ - [flush](#flush--gửi-ngay)
29
+ - [destroy](#destroy--dọn-dẹp)
30
+ - [Notification Inbox Tracking](#notification-inbox-tracking)
31
+ - [Cài đặt react-native-noti-sdk](#cài-đặt-react-native-noti-sdk)
32
+ - [Cấu hình domain](#cấu-hình-domain-notification-inbox)
33
+ - [API react-native-noti-sdk](#api-react-native-noti-sdk)
34
+ - [Custom notification UI](#custom-notification-ui-react-native-noti-sdk)
35
+ - [Tự động track delivered / open](#tự-động-track-delivered--open--bindnotificationtracking)
36
+ - [Track thủ công click / dismiss / read](#track-thủ-công-click--dismiss--read)
29
37
  - [Bảo mật](#bảo-mật)
30
- - [Dependencies](#dependencies)
38
+ - [HMAC-SHA256](#hmac-sha256-signature)
39
+ - [AES-256-CBC](#aes-256-cbc-encryption)
40
+ - [Best Practices](#best-practices)
41
+ - [Tích hợp React Navigation đầy đủ](#tích-hợp-react-navigation-đầy-đủ)
31
42
  - [API Reference](#api-reference)
32
43
 
33
44
  ---
@@ -35,49 +46,93 @@ CDP SDK cho React Native — tracking events và user profiles cho hệ thống
35
46
  ## Cài đặt
36
47
 
37
48
  ```bash
38
- npm install vc-cdp-rn-sdk @react-native-async-storage/async-storage react-native-device-info crypto-js
49
+ npm install vc-cdp-rn-sdk
39
50
  ```
40
51
 
41
- ### iOS
52
+ ### Peer Dependencies
53
+
54
+ ```bash
55
+ npm install @react-native-async-storage/async-storage
56
+ npm install react-native-device-info
57
+ ```
58
+
59
+ #### iOS
42
60
 
43
61
  ```bash
44
62
  cd ios && pod install
45
63
  ```
46
64
 
47
- ### Android
65
+ #### Android
48
66
 
49
- Không cần bước cấu hình thêm.
67
+ Không cần thêm cấu hình nếu dùng React Native >= 0.68 (auto-linking).
50
68
 
51
69
  ---
52
70
 
53
71
  ## Khởi tạo
54
72
 
55
- Gọi `CdpRnSdk.create()` **một lần duy nhất** tại entry point của app (ví dụ `App.tsx`). SDK dùng `AsyncStorage` nên init là bất đồng bộ.
73
+ Khởi tạo SDK **một lần duy nhất** tại entry point của app. SDK dùng `AsyncStorage` nên init là **async**.
56
74
 
57
75
  ```typescript
76
+ // src/lib/cdp.ts
58
77
  import { CdpRnSdk } from 'vc-cdp-rn-sdk';
59
78
 
60
- let cdp: CdpRnSdk;
61
-
62
- export async function setupCdp() {
63
- cdp = await CdpRnSdk.create({
64
- apiKey: 'YOUR_API_KEY', // Bắt buộc — do CDP cấp
65
- secretKey: 'YOUR_SECRET_KEY', // Bắt buộc — dùng ký HMAC + mã hoá AES
66
- source: 'DOP', // Bắt buộc — kênh phát triển (khai báo với CDP trước)
67
- serviceName: 'DOP', // Bắt buộc — DOP | LOS | LMS
68
-
69
- // Tuỳ chọn
70
- baseUrl: 'https://stg-ingestlog.vietcredit.com.vn', // mặc định: staging
71
- isTest: true, // đánh dấu môi trường test
72
- debug: true, // in log ra console
73
- batchSize: 10, // gửi batch khi queue đủ n events (mặc định: 10)
74
- batchInterval: 5000, // hoặc sau n ms dù chưa đủ batch (mặc định: 5000)
75
- enableEncryption: true // mã hoá PII bằng AES-256-CBC (mặc định: true)
79
+ let _cdp: CdpRnSdk | null = null;
80
+
81
+ export async function initCdp(): Promise<CdpRnSdk> {
82
+ _cdp = await CdpRnSdk.create({
83
+ apiKey: 'YOUR_API_KEY',
84
+ secretKey: 'YOUR_SECRET_KEY',
85
+ source: 'DOP',
86
+ serviceName: 'VC_APP',
87
+ baseUrl: 'https://ingestlog.vietcredit.com.vn',
88
+ debug: __DEV__,
76
89
  });
90
+ return _cdp;
91
+ }
92
+
93
+ export function getCdp(): CdpRnSdk {
94
+ if (!_cdp) throw new Error('CDP SDK chưa được khởi tạo. Gọi initCdp() trước.');
95
+ return _cdp;
96
+ }
97
+ ```
98
+
99
+ ```typescript
100
+ // App.tsx
101
+ import { useEffect } from 'react';
102
+ import { initCdp } from './lib/cdp';
103
+
104
+ export default function App() {
105
+ useEffect(() => {
106
+ initCdp();
107
+ }, []);
108
+ // ...
77
109
  }
78
110
  ```
79
111
 
80
- > **Lưu ý:** Các events được gọi trước khi `create()` hoàn thành sẽ được buffer lại tự động gửi sau khi khởi tạo xong không bị mất.
112
+ > **Lưu ý:** Mỗi lần `CdpRnSdk.create()` được gọi, SDK tự động tạo `session_id` mới tương ứng với 1 lần mở app.
113
+
114
+ ### Cấu hình đầy đủ
115
+
116
+ | Tham số | Kiểu | Mặc định | Mô tả |
117
+ |---|---|---|---|
118
+ | `apiKey` | `string` | — | **Bắt buộc.** API Key do CDP cấp |
119
+ | `secretKey` | `string` | — | **Bắt buộc.** Secret Key để ký HMAC và mã hoá AES |
120
+ | `source` | `string` | — | **Bắt buộc.** Kênh tích hợp (khai báo với CDP trước) |
121
+ | `serviceName` | `string` | — | **Bắt buộc.** `VC_APP` \| `DOP` \| `LOS` \| `LMS` |
122
+ | `baseUrl` | `string` | Staging URL | URL API CDP (không có `/v1`) |
123
+ | `isTest` | `boolean` | `false` | Đánh dấu môi trường test |
124
+ | `debug` | `boolean` | `false` | In log ra console |
125
+ | `batchSize` | `number` | `10` | Số events tối đa trong 1 batch |
126
+ | `batchInterval` | `number` | `5000` | Thời gian (ms) giữa các lần flush tự động |
127
+ | `enableEncryption` | `boolean` | `true` | Mã hoá PII bằng AES-256-CBC |
128
+ | `notificationApiBaseUrl` | `string` | — | Domain API notification-inbox (**khác** `baseUrl`) — dùng cho [`trackNotificationEvent()`](#track-thủ-công-click--dismiss--read) |
129
+
130
+ ### Môi trường
131
+
132
+ | Môi trường | baseUrl |
133
+ |---|---|
134
+ | Staging | `https://stg-ingestlog.vietcredit.com.vn` |
135
+ | Production | `https://ingestlog.vietcredit.com.vn` |
81
136
 
82
137
  ---
83
138
 
@@ -85,113 +140,77 @@ export async function setupCdp() {
85
140
 
86
141
  ```
87
142
  App khởi động
88
- └─> CdpRnSdk.create(config)
89
- │ - Tạo session_id mới (luôn fresh mỗi lần mở app)
90
- │ - Load anonymous_id, device_id từ AsyncStorage (persistent)
91
- │ - Load profile_id từ AsyncStorage (nếu đã login trước đó)
92
- │ - Thu thập device info (brand, model, OS)
143
+ └─> CdpRnSdk.create(config) ← async
144
+ │ - Load/tạo device_id → AsyncStorage (persist vĩnh viễn)
145
+ │ - Load/tạo anonymous_id AsyncStorage (persist vĩnh viễn)
146
+ │ - Tạo session_id mới → AsyncStorage (mỗi lần mở app)
147
+ │ - Load profile_id → AsyncStorage (nếu đã login)
93
148
 
94
149
  ├─ User CHƯA login
95
- │ └─> cdp.track(event)
150
+ │ └─> cdp.track() / cdp.screen()
96
151
  │ profile_id = null, gắn anonymous_id + device_id + session_id
97
- │ CDP sẽ identity-resolve khi user login sau
98
152
 
99
153
  └─ User login
100
154
  └─> cdp.identifyUser(userId, traits)
101
155
  └─> POST /v1/profiles/track → nhận profile_id
102
- └─> cdp.track(event)
103
- profile_id được gắn vào mọi event tiếp theo
156
+ └─> cdp.track() gắn profile_id vào mọi event
104
157
  ```
105
158
 
106
- **Session ID:** Mỗi lần mở app tạo một `session_id` mới. `device_id` và `anonymous_id` được giữ nguyên giữa các lần mở.
159
+ **Storage scope (AsyncStorage):**
160
+
161
+ | | Vòng đời |
162
+ |---|---|
163
+ | `device_id` | Vĩnh viễn (đến khi xoá app) |
164
+ | `anonymous_id` | Vĩnh viễn (rotate khi logout) |
165
+ | `session_id` | Tạo mới mỗi lần `create()` |
166
+ | `profile_id` | Xoá khi `destroy_session()` |
107
167
 
108
168
  ---
109
169
 
110
170
  ## User / Profile
111
171
 
112
- ### Đăng nhập (`identifyUser`)
113
-
114
- Gọi sau khi user đăng nhập thành công. SDK sẽ gọi `POST /v1/profiles/track`, nhận `profile_id` từ CDP và lưu vào storage để dùng cho các lần sau.
172
+ ### identifyUser — Đăng nhập
115
173
 
116
174
  ```typescript
117
- await cdp.identifyUser('user_123', {
118
- phone: '0901234567',
119
- full_name: 'Nguyễn Văn A',
120
- email: 'user@example.com',
121
- gender: 'M', // 'M' | 'F' | 'O'
122
- dob: '1990-01-15', // yyyy-MM-dd
123
- });
124
-
125
- // Tài khoản tổ chức (doanh nghiệp)
126
- await cdp.identifyUser('org_456', { full_name: 'Công ty ABC' }, 'ORG');
127
-
128
- // Alias ngắn gọn
129
- await cdp.identify('user_123', { phone: '0901234567' });
175
+ await cdp.identifyUser(userId, traits?, partyType?)
130
176
  ```
131
177
 
132
178
  | Tham số | Kiểu | Bắt buộc | Mô tả |
133
179
  |---|---|---|---|
134
- | `userId` | `string` | ✓ | Mã user trong hệ thống |
180
+ | `userId` | `string` | ✓ | Mã user trong hệ thống nội bộ |
135
181
  | `traits` | `UserTraits` | | Thông tin nhân khẩu học |
136
182
  | `partyType` | `'PERSON' \| 'ORG'` | | Mặc định `'PERSON'` |
137
183
 
138
- ---
139
-
140
- ### Cập nhật profile (`updateProfile`)
141
-
142
- Force-update profile theo `profile_id` — không trigger identity resolution. Dùng khi cần cập nhật một số trường cụ thể sau khi đã có `profile_id`.
143
-
144
- Gọi `POST /v1/profiles/update`.
145
-
146
184
  ```typescript
147
- await cdp.updateProfile({
148
- traits: {
149
- kyc_status: 'verified',
150
- mps_verified: true,
151
- },
152
- consents: [
153
- { consent_type: 'marketing_email', status: true, consent_date: '2025-06-01' },
154
- ],
185
+ // Cá nhân
186
+ await cdp.identifyUser('user_001', {
187
+ phone: '0901234567',
188
+ email: 'user@example.com',
189
+ full_name: 'Nguyễn Văn A',
190
+ gender: 'M',
191
+ dob: '1990-01-15',
192
+ idcard: '012345678901',
193
+ address: '123 Lê Lợi, Q.1, TP.HCM',
194
+ occupation: 'Kỹ sư phần mềm',
195
+ nationality: 'Vietnamese',
196
+ marital_status: 'single',
155
197
  });
156
198
 
157
- // Cập nhật profile_id khác (không phải profile hiện tại)
158
- await cdp.updateProfile({
159
- profile_id: 'cdp_profile_xyz',
160
- traits: { occupation: 'Kỹ sư' },
161
- });
162
- ```
163
-
164
- | Tham số | Kiểu | Mô tả |
165
- |---|---|---|
166
- | `profile_id` | `string` | Mặc định lấy profile_id hiện tại |
167
- | `traits` | `UserTraits` | Các trường cần update |
168
- | `consents` | `ConsentInfo[]` | Danh sách consent |
169
- | `scores` | `ScoreInfo[]` | Danh sách điểm |
170
- | `products` | `ProductInfo[]` | Danh sách sản phẩm |
171
- | `metadata` | `object` | Thông tin bổ sung |
172
-
173
- ---
174
-
175
- ### Lấy thông tin profile (`getProfile`)
199
+ console.log(cdp.getProfileId()); // "177671850461184000"
176
200
 
177
- Gọi `GET /v1/profiles/get/{profile_id}`.
178
-
179
- ```typescript
180
- // Lấy profile hiện tại
181
- const profile = await cdp.getProfile();
182
-
183
- // Hoặc chỉ định profile_id cụ thể
184
- const profile = await cdp.getProfile('cdp_profile_xyz');
185
-
186
- console.log(profile?.traits);
187
- console.log(profile?.consents);
201
+ // Tổ chức
202
+ await cdp.identifyUser('org_001', {
203
+ full_name: 'Công ty TNHH ABC',
204
+ phone: '0281234567',
205
+ email: 'contact@abc.vn',
206
+ }, 'ORG');
188
207
  ```
189
208
 
190
209
  ---
191
210
 
192
- ### Trait setters (chainable)
211
+ ### Trait Setters — Cập nhật từng thuộc tính
193
212
 
194
- Cập nhật từng thuộc tính riêng lẻ. Mỗi setter sẽ tự gọi `POST /v1/profiles/track` với `type: update`. Các setter có thể nối chuỗi.
213
+ Chainable. Mỗi setter gọi ngay `POST /v1/profiles/track` (type: update).
195
214
 
196
215
  ```typescript
197
216
  cdp
@@ -201,352 +220,879 @@ cdp
201
220
  .add_email('user@example.com')
202
221
  .add_mobile('0901234567')
203
222
  .add_gender('M')
204
- .add_birthday(new Date('1990-01-15')) // hoặc '1990-01-15'
223
+ .add_birthday('1990-01-15')
205
224
  .add_idcard('012345678901')
206
225
  .add_old_idcard('123456789')
207
- .add_address('123 Đường ABC, Nội')
226
+ .add_address('123 Lợi, Q.1, TP.HCM')
208
227
  .add_occupation('Kỹ sư phần mềm')
209
228
  .add_nationality('Vietnamese')
210
229
  .add_marital_status('single')
211
230
  .add_religion('Buddhist')
212
- .add_zalo_id('zalo_123')
213
- .add_tiktok_id('tiktok_abc');
214
-
215
- // Cập nhật nhiều thuộc tính cùng lúc
216
- cdp.add_user_attributes({
217
- occupation: 'Kỹ sư',
218
- nationality: 'Vietnamese',
219
- mps_verified: true,
220
- });
221
-
222
- // Cập nhật một thuộc tính tùy chỉnh
223
- cdp.add_user_attribute('custom_field', 'value');
231
+ .add_zalo_id('zalo_user_001')
232
+ .add_tiktok_id('tiktok_user_001');
224
233
  ```
225
234
 
226
- **Danh sách trait setters:**
227
-
228
- | Method | Trường trong `traits` | Ghi chú |
235
+ | Method | Trường | Ghi chú |
229
236
  |---|---|---|
230
237
  | `add_first_name(v)` | `first_name` | |
231
238
  | `add_last_name(v)` | `last_name` | |
232
239
  | `add_user_name(v)` | `full_name` | |
233
- | `add_email(v)` | `email` | PII — tự động mã hoá |
234
- | `add_mobile(v)` | `phone` | PII — tự động mã hoá |
240
+ | `add_email(v)` | `email` | 🔒 PII |
241
+ | `add_mobile(v)` | `phone` | 🔒 PII |
235
242
  | `add_gender(v)` | `gender` | `'M'` \| `'F'` \| `'O'` |
236
243
  | `add_birthday(v)` | `dob` | `Date` hoặc `'yyyy-MM-dd'` |
237
- | `add_idcard(v)` | `idcard` | PII — tự động mã hoá |
238
- | `add_old_idcard(v)` | `old_idcard` | PII — tự động mã hoá |
239
- | `add_address(v)` | `address` | PII — tự động mã hoá |
244
+ | `add_idcard(v)` | `idcard` | 🔒 PII |
245
+ | `add_old_idcard(v)` | `old_idcard` | 🔒 PII |
246
+ | `add_address(v)` | `address` | 🔒 PII |
240
247
  | `add_occupation(v)` | `occupation` | |
241
248
  | `add_nationality(v)` | `nationality` | |
242
249
  | `add_marital_status(v)` | `marital_status` | |
243
- | `add_religion(v)` | `religion` | PII — tự động mã hoá |
250
+ | `add_religion(v)` | `religion` | 🔒 PII |
244
251
  | `add_zalo_id(v)` | `zalo_id` | |
245
252
  | `add_tiktok_id(v)` | `tiktok_id` | |
246
- | `add_user_attribute(name, val)` | tùy chỉnh | |
247
- | `add_user_attributes(obj)` | nhiều trường | |
248
- | `setUserAttributes(obj)` | nhiều trường | alias |
253
+ | `add_user_attribute(name, val)` | custom | Bất kỳ trường nào |
254
+ | `add_user_attributes(obj)` | nhiều trường | Batch update |
255
+ | `setUserAttributes(obj)` | nhiều trường | Alias |
249
256
 
250
257
  ---
251
258
 
252
- ### Consents
259
+ ### updateProfile — Force-update
260
+
261
+ ```typescript
262
+ await cdp.updateProfile({
263
+ traits: { kyc_status: 'verified', mps_verified: true },
264
+ consents: [{ consent_type: 'marketing_sms', status: true, consent_date: '2025-06-01' }],
265
+ });
266
+ ```
267
+
268
+ ---
269
+
270
+ ### getProfile — Lấy thông tin profile
271
+
272
+ ```typescript
273
+ const profile = await cdp.getProfile();
274
+ const profile = await cdp.getProfile('cdp_123');
275
+ ```
253
276
 
254
- Lưu danh sách consent của user. Sẽ được đính kèm vào mọi lần gọi profile sau đó.
277
+ ---
278
+
279
+ ### Consents
255
280
 
256
281
  ```typescript
257
282
  cdp.setConsents([
258
- {
259
- consent_type: 'marketing_sms',
260
- status: true,
261
- consent_date: '2025-06-01',
262
- },
263
- {
264
- consent_type: 'marketing_email',
265
- status: false,
266
- },
283
+ { consent_type: 'marketing_sms', status: true, consent_date: '2025-06-01' },
284
+ { consent_type: 'marketing_email', status: false },
285
+ { consent_type: 'data_sharing', status: true },
267
286
  ]);
268
287
  ```
269
288
 
270
- | Trường | Kiểu | Bắt buộc | Mô tả |
271
- |---|---|---|---|
272
- | `consent_type` | `string` | ✓ | Loại consent |
273
- | `status` | `boolean` | ✓ | `true` = đồng ý |
274
- | `consent_date` | `string` | | Ngày đồng ý |
275
-
276
289
  ---
277
290
 
278
291
  ### Scores
279
292
 
280
- Lưu điểm tín dụng / rủi ro của user.
281
-
282
293
  ```typescript
283
294
  cdp.setScores([
284
- {
285
- score_type: 'credit_score',
286
- score_value: 720,
287
- score_date: '2025-06-01',
288
- },
289
- {
290
- score_type: 'risk_score',
291
- score_value: 85,
292
- },
295
+ { score_type: 'credit_score', score_value: 720, score_date: '2025-06-01' },
293
296
  ]);
294
297
  ```
295
298
 
296
- | Trường | Kiểu | Bắt buộc | Mô tả |
297
- |---|---|---|---|
298
- | `score_type` | `string` | ✓ | Loại điểm |
299
- | `score_value` | `number` | ✓ | Giá trị |
300
- | `score_date` | `string` | | Ngày tính điểm |
301
-
302
299
  ---
303
300
 
304
301
  ### Products
305
302
 
306
- Lưu danh sách sản phẩm mà user đang sử dụng.
307
-
308
303
  ```typescript
309
304
  cdp.setProducts([
310
- {
311
- product_code: 'LOAN_CONSUMER',
312
- product_name: 'Vay tiêu dùng',
313
- status: 'active',
314
- },
315
- {
316
- product_code: 'CREDIT_CARD',
317
- product_name: 'Thẻ tín dụng',
318
- status: 'pending',
319
- },
305
+ { product_code: 'LOAN_CONSUMER', product_name: 'Vay tiêu dùng', status: 'active' },
320
306
  ]);
321
307
  ```
322
308
 
323
- | Trường | Kiểu | Bắt buộc | Mô tả |
324
- |---|---|---|---|
325
- | `product_code` | `string` | ✓ | Mã sản phẩm |
326
- | `product_name` | `string` | | Tên sản phẩm |
327
- | `status` | `string` | | Trạng thái |
328
-
329
309
  ---
330
310
 
331
- ### Metadata
332
-
333
- Gắn thêm thông tin tùy chỉnh vào tất cả các payload (profile + event).
311
+ ### Metadata, Campaign, Push Token
334
312
 
335
313
  ```typescript
336
- cdp.setMetadata({
337
- schema_version: '2.0',
338
- app_flavor: 'production',
339
- referral_code: 'REF123',
314
+ cdp.setMetadata({ schema_version: '2.0', referral_code: 'REF123' });
315
+
316
+ cdp.setCampaign({
317
+ utm_source: 'facebook',
318
+ utm_medium: 'cpc',
319
+ utm_campaign: 'summer_loan_2025',
340
320
  });
321
+
322
+ // Push notification token (Expo hoặc FCM)
323
+ cdp.setPushToken('ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]');
324
+ cdp.setPushToken('fcm_token_abc123...');
341
325
  ```
342
326
 
343
327
  ---
344
328
 
345
- ### Campaign / UTM
329
+ ### getUserIdentities
330
+
331
+ ```typescript
332
+ const ids = cdp.getUserIdentities();
333
+ // {
334
+ // user_id: 'user_001',
335
+ // profile_id: '177671850461184000',
336
+ // anonymous_id: 'uuid-...',
337
+ // device_id: 'uuid-...',
338
+ // session_id: 'uuid-...',
339
+ // }
340
+
341
+ cdp.getProfileId();
342
+ cdp.getDeviceId();
343
+ cdp.getSessionId();
344
+ cdp.getUserAttribute('phone');
345
+ ```
346
+
347
+ ---
346
348
 
347
- Gắn thông tin chiến dịch vào tất cả event và profile call tiếp theo.
349
+ ### destroy_session Đăng xuất
348
350
 
349
351
  ```typescript
350
- cdp.setCampaign({
351
- utm_source: 'facebook',
352
- utm_medium: 'cpc',
353
- utm_campaign: 'summer_loan_2025',
354
- utm_term: 'vay_tieu_dung',
355
- utm_content: 'banner_top',
356
- });
352
+ await cdp.destroy_session();
353
+ // hoặc alias:
354
+ await cdp.reset();
357
355
  ```
358
356
 
357
+ | | Hành động |
358
+ |---|---|
359
+ | `userId`, `profileId`, `userTraits` | Xoá |
360
+ | `consents`, `scores`, `products`, `campaign` | Xoá |
361
+ | `anonymous_id` | Tạo mới, lưu AsyncStorage |
362
+ | `session_id` | Tạo mới, lưu AsyncStorage |
363
+ | `device_id` | **Giữ nguyên** |
364
+
359
365
  ---
360
366
 
361
- ### Push Token
367
+ ## Event Tracking
362
368
 
363
- Lưu FCM token (Android) hoặc APNs token (iOS) để CDP dùng cho push notification.
369
+ ### track Custom event
364
370
 
365
371
  ```typescript
366
- cdp.setPushToken('FCM_OR_APNS_TOKEN_HERE');
372
+ cdp.track(eventName, properties?)
373
+ ```
374
+
375
+ ```typescript
376
+ cdp.track('loan_viewed', {
377
+ loan_id: 'LOAN_001',
378
+ amount: 50_000_000,
379
+ term: 24,
380
+ type: 'consumer',
381
+ });
382
+
383
+ cdp.track('button_click', { button: 'apply_now', screen: 'LoanDetailScreen' });
384
+
385
+ cdp.track('form_submitted', { form: 'loan_application', success: true });
367
386
  ```
368
387
 
369
388
  ---
370
389
 
371
- ### Đăng xuất
390
+ ### screen — Màn hình
372
391
 
373
- Gửi event logout, xoá toàn bộ dữ liệu user, rotate `anonymous_id` và `session_id`. `device_id` được giữ lại.
392
+ Track lượt xem màn hình. Gửi event `screen_view`.
374
393
 
375
394
  ```typescript
376
- await cdp.destroy_session();
395
+ cdp.screen('HomeScreen');
396
+ cdp.screen('LoanDetailScreen', { loan_code: 'CONSUMER_24M' });
397
+ cdp.screen('ProfileScreen', { tab: 'personal_info' });
398
+ ```
377
399
 
378
- // Alias
379
- await cdp.reset();
400
+ ---
401
+
402
+ ### Tích hợp React Navigation
403
+
404
+ ```typescript
405
+ // App.tsx
406
+ import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
407
+ import { getCdp } from './lib/cdp';
408
+
409
+ export default function App() {
410
+ const navRef = useNavigationContainerRef();
411
+
412
+ return (
413
+ <NavigationContainer
414
+ ref={navRef}
415
+ onReady={() => {
416
+ const name = navRef.getCurrentRoute()?.name;
417
+ if (name) getCdp().screen(name);
418
+ }}
419
+ onStateChange={() => {
420
+ const name = navRef.getCurrentRoute()?.name;
421
+ if (name) getCdp().screen(name);
422
+ }}
423
+ >
424
+ {/* Stack / Tab / Drawer navigators */}
425
+ </NavigationContainer>
426
+ );
427
+ }
380
428
  ```
381
429
 
382
- **Sau khi logout:**
383
- - `userId`, `profileId`, `userTraits`, `consents`, `scores`, `products`, `campaign` → xoá
384
- - `anonymousId`, `sessionId` → tạo mới
385
- - `deviceId` → giữ nguyên
430
+ ---
431
+
432
+ ### Batch Processing
433
+
434
+ Events được gom vào **queue** rồi flush theo điều kiện:
435
+
436
+ | Điều kiện | Mô tả |
437
+ |---|---|
438
+ | `batchSize` đạt ngưỡng | Mặc định 10 events |
439
+ | `batchInterval` timeout | Mặc định 5 000 ms |
440
+ | `flush()` gọi thủ công | Ngay lập tức |
441
+ | App vào background | Gọi `flush()` trước `AppState` change |
386
442
 
387
443
  ---
388
444
 
389
- ## Event Tracking
445
+ ### flush — Gửi ngay
390
446
 
391
- ### Track sự kiện
447
+ ```typescript
448
+ await cdp.flush();
449
+ ```
392
450
 
393
451
  ```typescript
394
- cdp.track('button_click', {
395
- button_name: 'apply_loan',
396
- screen: 'HomeScreen',
397
- loan_amount: 50_000_000,
398
- });
452
+ // Flush khi app vào background
453
+ import { AppState } from 'react-native';
399
454
 
400
- cdp.track('form_submit', {
401
- form_name: 'personal_info',
402
- step: 2,
403
- duration_seconds: 45,
455
+ AppState.addEventListener('change', async (state) => {
456
+ if (state === 'background') {
457
+ await cdp.flush();
458
+ }
404
459
  });
405
460
  ```
406
461
 
407
- > **Batching:** Events được gom theo `batchSize` hoặc `batchInterval`, sau đó gửi một lần qua `POST /v2/events/batch`. Nếu `batchSize = 1`, mỗi event được gửi ngay qua `POST /v2/events/track`.
462
+ ---
408
463
 
409
- Mỗi event tự động bao gồm:
464
+ ### destroy Dọn dẹp
410
465
 
411
- | Trường | Nguồn |
412
- |---|---|
413
- | `event_id` | UUID mới mỗi event |
414
- | `session_id` | Session hiện tại |
415
- | `device_id` | Persistent per device |
416
- | `profile_id` | Từ CDP sau khi login |
417
- | `anonymous_id` | Persistent, rotate khi logout |
418
- | `platforms` | Từ `react-native-device-info` |
419
- | `context.locale` | Ngôn ngữ thiết bị |
420
- | `context.timezone` | Múi giờ thiết bị |
421
- | `context.app.version` | Phiên bản app |
422
- | `event_time` | ISO 8601 tại thời điểm gọi |
466
+ ```typescript
467
+ await cdp.destroy();
468
+ ```
469
+
470
+ Flush queue còn lại xoá timer. Không xoá user data.
423
471
 
424
472
  ---
425
473
 
426
- ### Track màn hình (`screen`)
474
+ ## Notification Inbox Tracking
427
475
 
428
- ```typescript
429
- cdp.screen('HomeScreen');
476
+ Tích hợp [`react-native-noti-sdk`](https://www.npmjs.com/package/react-native-noti-sdk)
477
+ (FCM + Notifee) với `vc-cdp-rn-sdk` để tracking các sự kiện thông báo
478
+ (`delivered` / `open` / `click` / `dismiss` / `read`) qua notification-inbox API.
430
479
 
431
- cdp.screen('LoanDetailScreen', {
432
- loan_id: 'LOAN_001',
433
- loan_amount: 50_000_000,
480
+ > API notification-inbox nằm trên **domain khác** với API ingest event/profile
481
+ > → cấu hình riêng qua `notificationApiBaseUrl` (xem [Cấu hình đầy đủ](#cấu-hình-đầy-đủ)).
482
+
483
+ ### Cài đặt react-native-noti-sdk
484
+
485
+ ```bash
486
+ npm install react-native-noti-sdk
487
+ npm install @react-native-firebase/app @react-native-firebase/messaging @notifee/react-native @react-native-clipboard/clipboard
488
+ ```
489
+
490
+ ```bash
491
+ cd ios && pod install
492
+ ```
493
+
494
+ **Cấu hình Firebase (phía app tích hợp):**
495
+
496
+ - **Android:** thêm `android/app/google-services.json` + plugin `com.google.gms.google-services`.
497
+ - **iOS:** thêm `GoogleService-Info.plist` vào Xcode project, bật capability **Push Notifications** + **Background Modes > Remote notifications**.
498
+
499
+ **Đăng ký background handler (bắt buộc)** — gọi ở top-level `index.js`, **trước** `registerComponent`:
500
+
501
+ ```js
502
+ // index.js
503
+ import NotiSdk from 'react-native-noti-sdk';
504
+ NotiSdk.registerBackgroundHandler();
505
+ ```
506
+
507
+ ### Cấu hình domain notification-inbox
508
+
509
+ ```typescript
510
+ const cdp = await CdpRnSdk.create({
511
+ apiKey: 'YOUR_API_KEY',
512
+ secretKey: 'YOUR_SECRET_KEY',
513
+ source: 'DOP',
514
+ serviceName: 'VC_APP',
515
+ baseUrl: 'https://ingestlog.vietcredit.com.vn', // API events/profile
516
+
517
+ notificationApiBaseUrl: 'https://noti.vietcredit.com.vn', // API notification-inbox — domain khác
434
518
  });
435
519
  ```
436
520
 
437
- Tương đương với:
521
+ Nếu không set `notificationApiBaseUrl`, `trackNotificationEvent()` sẽ bỏ qua và log lỗi (không throw).
522
+
523
+ ### API react-native-noti-sdk
524
+
438
525
  ```typescript
439
- cdp.track('screen_view', { screen_name: 'HomeScreen', ...properties });
526
+ import NotiSdk from 'react-native-noti-sdk';
527
+
528
+ await NotiSdk.init({ debug: true }); // xin quyền, tạo channel, lắng nghe
529
+ const token = await NotiSdk.getToken(); // FCM token gửi lên server
530
+ NotiSdk.onTokenRefresh(t => sendTokenToServer(t)); // token được làm mới
531
+
532
+ NotiSdk.onMessage(msg => console.log(msg.title, msg.data)); // app đang mở
533
+ NotiSdk.onNotificationOpened(msg => navigate(msg.data.screen)); // tap mở app từ background
534
+
535
+ const initial = await NotiSdk.getInitialNotification(); // mở app từ trạng thái quit
536
+ if (initial) navigate(initial.data.screen);
537
+
538
+ await NotiSdk.displayNotification({ title: 'Xin chào', body: 'Thông báo', data: { screen: 'home' } });
440
539
  ```
441
540
 
442
- ---
541
+ | Hàm | Mô tả |
542
+ | --- | --- |
543
+ | `init(config?)` | Khởi tạo SDK: xin quyền, đăng ký iOS, tạo channel, lắng nghe. Idempotent. |
544
+ | `requestPermission()` | Hiển thị hộp thoại xin quyền, trả về `AuthorizationStatus`. |
545
+ | `hasPermission()` | Trạng thái quyền hiện tại (không hiện hộp thoại). |
546
+ | `getToken()` | Trả về FCM token (`string`) hoặc `null` nếu lỗi. |
547
+ | `deleteToken()` | Xoá token hiện tại; lần `getToken()` sau sẽ tạo token mới. |
548
+ | `onTokenRefresh(listener)` | Lắng nghe token mới; trả về `unsubscribe`. |
549
+ | `registerBackgroundHandler()` | Đăng ký nhận message ở background. Gọi tại `index.js`. |
550
+ | `onMessage(handler)` | Nhận `NotiMessage` khi có message; trả về `unsubscribe`. |
551
+ | `onNotificationOpened(handler)` | Khi tap thông báo mở app (background); trả về `unsubscribe`. |
552
+ | `getInitialNotification()` | Thông báo đã mở app từ trạng thái quit, hoặc `null`. |
553
+ | `displayNotification(options)` | Tự hiển thị một local notification qua Notifee. |
554
+ | `displayCustomNotification(options)` | Hiển thị notification UI tuỳ biến (native), có nút hành động. |
555
+ | `isCustomUiAvailable()` | `true` nếu native module custom UI đang hoạt động. |
556
+
557
+ **`NotiSdkConfig`**
558
+
559
+ | Trường | Mặc định | Mô tả |
560
+ | --- | --- | --- |
561
+ | `requestPermissionOnInit` | `true` | Tự xin quyền khi `init()`. |
562
+ | `debug` | `false` | Bật log `[NotiSdk]` ra console. |
563
+ | `defaultChannel` | `{ id: 'default', name: 'Mặc định', importance: 'high' }` | Channel Android mặc định. |
564
+ | `displayInForeground` | `true` | Tự hiện banner khi nhận message lúc app đang mở. |
565
+
566
+ **`NotiMessage`**
567
+
568
+ ```typescript
569
+ interface NotiMessage {
570
+ id?: string;
571
+ title?: string;
572
+ body?: string;
573
+ data: Record<string, string>;
574
+ }
575
+ ```
443
576
 
444
- ### Flush thủ công
577
+ **`AuthorizationStatus`**: `'authorized' | 'provisional' | 'denied' | 'notDetermined'`
445
578
 
446
- Gửi toàn bộ events đang trong queue ngay lập tức. Hữu ích khi app sắp vào background hoặc trước khi đăng xuất.
579
+ > **Android 13+**: SDK tự xin quyền `POST_NOTIFICATIONS` qua Notifee khi gọi
580
+ > `requestPermission()` / `init()`. Đảm bảo `compileSdkVersion >= 33`.
581
+
582
+ ### Custom notification UI (react-native-noti-sdk)
447
583
 
448
584
  ```typescript
449
- await cdp.flush();
585
+ await NotiSdk.displayCustomNotification({
586
+ title: 'Custom UI',
587
+ body: 'Layout tuỳ biến + nút hành động',
588
+ imageUrl: 'https://example.com/banner.png',
589
+ fullScreen: false, // true: full-screen intent (vd cuộc gọi đến)
590
+ data: { screen: 'detail', tracking_id: 'b7e6c1a2-...' },
591
+ buttons: [
592
+ { name: 'Gọi điện', type: 'phone', value: '03658700512' },
593
+ { name: 'Đầu tư', type: 'deep_link', value: 'tinvay://notification' },
594
+ ],
595
+ });
450
596
  ```
451
597
 
452
- ---
598
+ | `type` | Hành vi khi nhấn |
599
+ | --- | --- |
600
+ | `phone` | Mở trình quay số với `value` (`ACTION_DIAL`, không cần quyền gọi). |
601
+ | `deep_link` | Mở URI `value`. App phải khai báo `intent-filter` cho scheme đó. |
453
602
 
454
- ## Lấy thông tin định danh
603
+ > Android giới hạn tối đa **3 nút**. Cả hai nền tảng dùng native module riêng
604
+ > (không phụ thuộc Notifee cho phần custom UI) — tự đăng ký qua autolinking.
605
+ > iOS cần `pod install`; hiển thị ảnh yêu cầu quyền thông báo đã cấp; nút
606
+ > share mở `UIActivityViewController` khi app ở foreground.
607
+
608
+ ### Tự động track delivered / open — bindNotificationTracking
609
+
610
+ `bindNotificationTracking()` gắn `react-native-noti-sdk` vào CDP SDK: tự động
611
+ gọi `trackNotificationEvent()` khi nhận message (`delivered`) và khi user tap
612
+ mở app từ thông báo (`open`), miễn là payload push có field `tracking_id`
613
+ trong `data`.
455
614
 
456
615
  ```typescript
457
- const ids = cdp.getUserIdentities();
458
- // {
459
- // user_id: 'user_123',
460
- // profile_id: 'cdp_abc123',
461
- // anonymous_id: 'uuid-...',
462
- // device_id: 'uuid-...',
463
- // session_id: 'uuid-...',
464
- // }
616
+ import NotiSdk from 'react-native-noti-sdk';
617
+ import { getCdp } from './lib/cdp';
618
+
619
+ async function setupPush() {
620
+ await NotiSdk.init({ debug: true });
621
+
622
+ // onMessage → 'delivered', onNotificationOpened/getInitialNotification → 'open'
623
+ const unbind = getCdp().bindNotificationTracking(NotiSdk);
624
+
625
+ // unbind() khi cleanup nếu cần
626
+ }
627
+ ```
628
+
629
+ Payload push phải nhúng `tracking_id`, ví dụ FCM `data`:
630
+
631
+ ```json
632
+ {
633
+ "tracking_id": "b7e6c1a2-...",
634
+ "deeplink": "app://home"
635
+ }
636
+ ```
637
+
638
+ ### Track thủ công click / dismiss / read
465
639
 
466
- cdp.getProfileId(); // profile_id từ CDP
467
- cdp.getDeviceId(); // persistent device identifier
468
- cdp.getSessionId(); // session hiện tại (mới mỗi lần mở app)
640
+ `react-native-noti-sdk` **không callback riêng** cho các sự kiện này (nhấn
641
+ nút trong `displayCustomNotification`, dismiss, hoặc đọc trong màn hình
642
+ inbox) — gọi `trackNotificationEvent()` trực tiếp tại nơi xử lý:
469
643
 
470
- cdp.getUserAttribute('phone'); // lấy giá trị trait cụ thể
644
+ ```typescript
645
+ // Khi user nhấn nút trong custom notification
646
+ cdp.trackNotificationEvent(trackingId, 'click', {
647
+ buttonId: 'btn_1',
648
+ deeplink: 'app://home',
649
+ currentScreen: 'HomeScreen',
650
+ });
651
+
652
+ // Khi user dismiss thông báo
653
+ cdp.trackNotificationEvent(trackingId, 'dismiss');
654
+
655
+ // Khi user đọc thông báo trong màn hình inbox
656
+ cdp.trackNotificationEvent(trackingId, 'read', { currentScreen: 'NotificationInboxScreen' });
657
+ ```
658
+
659
+ `trackNotificationEvent()` gửi `POST {notificationApiBaseUrl}/api/v1/notification-inbox/track-by-tracking-id`.
660
+ Endpoint này **không** dùng HMAC signature như API ingest — chỉ header
661
+ `Content-Type: application/json`.
662
+
663
+ ```typescript
664
+ async trackNotificationEvent(
665
+ trackingId: string,
666
+ eventType: 'delivered' | 'open' | 'click' | 'dismiss' | 'read',
667
+ options?: {
668
+ userAppId?: string; // mặc định: user_id hiện tại (identifyUser), '' nếu chưa login
669
+ userProfileId?: string; // mặc định: profile_id hiện tại, '' nếu chưa có profile_id
670
+ deviceId?: string; // mặc định: device_id của SDK
671
+ platform?: string; // mặc định: Platform.OS ('android' | 'ios')
672
+ buttonId?: string;
673
+ deeplink?: string;
674
+ currentScreen?: string;
675
+ metadata?: unknown[];
676
+ }
677
+ ): Promise<{ code?: number; message?: string } | undefined>
471
678
  ```
472
679
 
473
680
  ---
474
681
 
475
682
  ## Bảo mật
476
683
 
477
- ### HMAC Signature
684
+ ### HMAC-SHA256 Signature
685
+
686
+ Mọi request đến CDP API đều được ký tự động.
478
687
 
479
- Mọi request tự động được ký. Header `X-Signatures` được tính theo công thức:
688
+ **Headers:**
480
689
 
481
690
  ```
482
- X-Signatures = Base64(HMAC-SHA256(source + "|" + timestamp + "|" + payload))
691
+ X-Api-Key: <apiKey>
692
+ X-Source: <source>
693
+ X-Timestamp: <unix_timestamp_s>
694
+ X-Signatures: <base64_signature>
695
+ X-Trace-Id: <uuid>
696
+ Content-Type: application/json
697
+ isTest: false
483
698
  ```
484
699
 
485
- ### Mã hoá PII (AES-256-CBC)
700
+ **Công thức:**
701
+
702
+ ```
703
+ signature = Base64( HMAC-SHA256(secretKey, message) )
704
+ message = source + "|" + timestamp + "|" + JSON(payload)
705
+ ```
706
+
707
+ > **Lưu ý:** RN SDK dùng timestamp tính bằng **giây** (khác với Web SDK dùng milliseconds).
708
+
709
+ ---
710
+
711
+ ### AES-256-CBC Encryption
712
+
713
+ Khi `enableEncryption: true`, các trường PII được mã hoá.
486
714
 
487
- Khi `enableEncryption: true`, các trường nhạy cảm sau được hoá trước khi gửi:
715
+ **Các trường PII bịhoá:**
488
716
 
489
- `full_name`, `first_name`, `last_name`, `phone`, `email`, `idcard`, `old_idcard`, `gender`, `dob`, `address`, `religion`
717
+ | Trường | tả |
718
+ |---|---|
719
+ | `phone` | Số điện thoại |
720
+ | `email` | Email |
721
+ | `idcard` | CMND/CCCD mới |
722
+ | `old_idcard` | CMND cũ |
723
+ | `address` | Địa chỉ |
724
+ | `religion` | Tôn giáo |
725
+ | `full_name` | Họ tên đầy đủ |
726
+ | `first_name` | Tên |
727
+ | `last_name` | Họ |
728
+ | `gender` | Giới tính |
729
+ | `dob` | Ngày sinh |
730
+
731
+ **Công thức:**
490
732
 
491
- Công thức mã hoá:
492
733
  ```
493
- traits = Base64(IV + AES_CBC_PKCS5(key, plaintext_traits_json))
494
- Key = SHA256(secretKey + X-Timestamp) → 32 bytes (AES-256)
495
- IV = Random 16 bytes
734
+ encrypted = Base64( IV[16 bytes] || AES_CBC_PKCS7( key, IV, plaintext ) )
735
+
736
+ key = SHA256( secretKey + timestamp )
737
+ IV = random 16 bytes (prepend vào ciphertext)
738
+ plaintext = JSON.stringify({ chỉ các trường PII })
496
739
  ```
497
740
 
498
- Sử dụng thư viện `crypto-js` pure JavaScript, không cần link native, hoạt động trên cả iOS và Android.
741
+ > **Lưu ý:** Không bật trên dev/staging nếu server chưa cấu hình giải mã. Dùng `enableEncryption: false`.
499
742
 
500
743
  ---
501
744
 
502
- ## Dependencies
745
+ ### Best Practices
503
746
 
504
- | Package | do |
505
- |---|---|
506
- | `@react-native-async-storage/async-storage` | Lưu `device_id`, `anonymous_id`, `profile_id` |
507
- | `react-native-device-info` | Lấy `brand`, `model`, `os_version`, `app_version` |
508
- | `crypto-js` | HMAC-SHA256 signature + AES-256-CBC encryption (pure JS) |
747
+ - Không log `secretKey` hay `apiKey` vào console production.
748
+ - Bật `debug: false` trên production build.
749
+ - Rotate key theo chính sách CDP team.
750
+ - Dùng environment variables, không hardcode credentials.
751
+
752
+ ```typescript
753
+ // ✅ Đúng — dùng env vars
754
+ import Config from 'react-native-config';
755
+
756
+ const cdp = await CdpRnSdk.create({
757
+ apiKey: Config.CDP_API_KEY,
758
+ secretKey: Config.CDP_SECRET_KEY,
759
+ source: 'DOP',
760
+ serviceName: 'VC_APP',
761
+ debug: __DEV__,
762
+ });
763
+ ```
764
+
765
+ ```bash
766
+ # .env
767
+ CDP_API_KEY=YOUR_API_KEY
768
+ CDP_SECRET_KEY=YOUR_SECRET_KEY
769
+ CDP_BASE_URL=https://stg-ingestlog.vietcredit.com.vn
770
+
771
+ # .env.production
772
+ CDP_API_KEY=<production_key>
773
+ CDP_SECRET_KEY=<production_secret>
774
+ CDP_BASE_URL=https://ingestlog.vietcredit.com.vn
775
+ ```
509
776
 
510
777
  ---
511
778
 
512
- ## API Reference
779
+ ## Tích hợp React Navigation đầy đủ
513
780
 
514
- ### `CdpRnSdk.create(config)`
781
+ Setup hoàn chỉnh với auto screen tracking và AppState flush:
515
782
 
516
- | Tham số | Kiểu | Bắt buộc | Mặc định |
517
- |---|---|---|---|
518
- | `apiKey` | `string` | ✓ | |
519
- | `secretKey` | `string` | ✓ | |
520
- | `source` | `string` | | |
521
- | `serviceName` | `string` | ✓ | |
522
- | `baseUrl` | `string` | | `https://stg-ingestlog.vietcredit.com.vn` |
523
- | `isTest` | `boolean` | | `false` |
524
- | `debug` | `boolean` | | `false` |
525
- | `batchSize` | `number` | | `10` |
526
- | `batchInterval` | `number` (ms) | | `5000` |
527
- | `enableEncryption` | `boolean` | | `true` |
528
-
529
- ### Endpoint mapping
530
-
531
- | Method SDK | HTTP | Endpoint |
532
- |---|---|---|
533
- | `identifyUser()` / `identify()` | POST | `/v1/profiles/track` |
534
- | `updateProfile()` | POST | `/v1/profiles/update` |
535
- | `getProfile()` | GET | `/v1/profiles/get/{profile_id}` |
536
- | `track()` (batchSize=1) | POST | `/v2/events/track` |
537
- | `track()` / `flush()` (batch) | POST | `/v2/events/batch` |
538
- | Trait setters | POST | `/v1/profiles/track` (type: update) |
539
- | `destroy_session()` | POST | `/v1/profiles/track` (type: logout) |
783
+ ```typescript
784
+ // src/lib/cdp.ts
785
+ import { CdpRnSdk } from 'vc-cdp-rn-sdk';
786
+
787
+ let _cdp: CdpRnSdk | null = null;
788
+
789
+ export async function initCdp(): Promise<CdpRnSdk> {
790
+ _cdp = await CdpRnSdk.create({
791
+ apiKey: process.env.CDP_API_KEY!,
792
+ secretKey: process.env.CDP_SECRET_KEY!,
793
+ source: 'DOP',
794
+ serviceName: 'VC_APP',
795
+ baseUrl: process.env.CDP_BASE_URL,
796
+ debug: __DEV__,
797
+ });
798
+ return _cdp;
799
+ }
800
+
801
+ export function getCdp(): CdpRnSdk {
802
+ if (!_cdp) throw new Error('CDP SDK chưa được khởi tạo. Gọi initCdp() trước.');
803
+ return _cdp;
804
+ }
805
+ ```
806
+
807
+ ```typescript
808
+ // App.tsx
809
+ import React, { useEffect, useRef } from 'react';
810
+ import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
811
+ import { AppState, AppStateStatus } from 'react-native';
812
+ import { initCdp, getCdp } from './lib/cdp';
813
+
814
+ export default function App() {
815
+ const navRef = useNavigationContainerRef();
816
+ const appState = useRef(AppState.currentState);
817
+
818
+ useEffect(() => {
819
+ initCdp();
820
+ }, []);
821
+
822
+ // Flush khi app vào background
823
+ useEffect(() => {
824
+ const sub = AppState.addEventListener('change', async (next: AppStateStatus) => {
825
+ if (appState.current === 'active' && next.match(/inactive|background/)) {
826
+ await getCdp().flush();
827
+ }
828
+ appState.current = next;
829
+ });
830
+ return () => sub.remove();
831
+ }, []);
832
+
833
+ return (
834
+ <NavigationContainer
835
+ ref={navRef}
836
+ onReady={() => {
837
+ const name = navRef.getCurrentRoute()?.name;
838
+ if (name) getCdp().screen(name);
839
+ }}
840
+ onStateChange={() => {
841
+ const name = navRef.getCurrentRoute()?.name;
842
+ if (name) getCdp().screen(name);
843
+ }}
844
+ >
845
+ <RootNavigator />
846
+ </NavigationContainer>
847
+ );
848
+ }
849
+ ```
850
+
851
+ ```typescript
852
+ // Hook tiện lợi
853
+ // hooks/useCdp.ts
854
+ import { getCdp } from '../lib/cdp';
855
+ import type { CdpRnSdk } from 'vc-cdp-rn-sdk';
856
+
857
+ export function useCdp(): CdpRnSdk {
858
+ return getCdp();
859
+ }
860
+ ```
861
+
862
+ ```typescript
863
+ // Identify sau đăng nhập
864
+ async function handleLogin(phone: string, password: string) {
865
+ const user = await authApi.login(phone, password);
866
+ await getCdp().identifyUser(user.id, {
867
+ phone: user.phone,
868
+ email: user.email,
869
+ full_name: user.name,
870
+ });
871
+ navigation.navigate('Home');
872
+ }
873
+
874
+ // Đăng xuất
875
+ async function handleLogout() {
876
+ await authApi.logout();
877
+ await getCdp().destroy_session();
878
+ navigation.reset({ index: 0, routes: [{ name: 'Login' }] });
879
+ }
880
+ ```
540
881
 
541
882
  ---
542
883
 
543
- ## Tài liệu chi tiết
884
+ ## API Reference
544
885
 
545
- | File | Nội dung |
546
- |---|---|
547
- | [docs/getting-started.md](docs/getting-started.md) | Hướng dẫn bắt đầu từ đầu |
548
- | [docs/profile.md](docs/profile.md) | Profile, traits, consents, scores, products |
549
- | [docs/events.md](docs/events.md) | Event tracking, batching, screen view |
550
- | [docs/security.md](docs/security.md) | HMAC signature, AES encryption |
551
- | [docs/api-reference.md](docs/api-reference.md) | Toàn bộ API với type signatures |
552
- | [docs/integration-react-navigation.md](docs/integration-react-navigation.md) | React Navigation integration |
886
+ ### Factory
887
+
888
+ ```typescript
889
+ static async create(config: CdpRnConfig): Promise<CdpRnSdk>
890
+ ```
891
+
892
+ ### CdpRnConfig
893
+
894
+ ```typescript
895
+ interface CdpRnConfig {
896
+ apiKey: string; // bắt buộc
897
+ secretKey: string; // bắt buộc
898
+ source: string; // bắt buộc — kênh tích hợp
899
+ serviceName: string; // bắt buộc — VC_APP | DOP | LOS | LMS
900
+ baseUrl?: string; // mặc định staging
901
+ isTest?: boolean; // mặc định false
902
+ debug?: boolean; // mặc định false
903
+ batchSize?: number; // mặc định 10
904
+ batchInterval?: number; // mặc định 5000 ms
905
+ enableEncryption?: boolean; // mặc định true
906
+ notificationApiBaseUrl?: string; // domain API notification-inbox (khác baseUrl)
907
+ }
908
+ ```
909
+
910
+ ### Profile Methods
911
+
912
+ ```typescript
913
+ async identifyUser(userId: string, traits?: UserTraits, partyType?: 'PERSON' | 'ORG'): Promise<this>
914
+ async identify(userId: string, traits?: UserTraits): Promise<this> // alias
915
+
916
+ async updateProfile(payload: {
917
+ profile_id?: string;
918
+ traits?: UserTraits;
919
+ consents?: ConsentInfo[];
920
+ scores?: ScoreInfo[];
921
+ products?: ProductInfo[];
922
+ metadata?: Record<string, unknown>;
923
+ }): Promise<ProfileResponse | undefined>
924
+
925
+ async getProfile(profileId?: string): Promise<ProfileData | undefined>
926
+ ```
927
+
928
+ ### Trait Setter Methods (chainable → `this`)
929
+
930
+ ```typescript
931
+ add_first_name(v: string): this
932
+ add_last_name(v: string): this
933
+ add_user_name(v: string): this
934
+ add_email(v: string): this
935
+ add_mobile(v: string): this
936
+ add_gender(v: 'M' | 'F' | 'O'): this
937
+ add_birthday(v: string | Date): this
938
+ add_idcard(v: string): this
939
+ add_old_idcard(v: string): this
940
+ add_address(v: string): this
941
+ add_occupation(v: string): this
942
+ add_nationality(v: string): this
943
+ add_marital_status(v: string): this
944
+ add_religion(v: string): this
945
+ add_zalo_id(v: string): this
946
+ add_tiktok_id(v: string): this
947
+ add_user_attribute(name: string, value: unknown): this
948
+ add_user_attributes(attrs: UserTraits): this
949
+ setUserAttributes(attrs: UserTraits): this
950
+ ```
951
+
952
+ ### Consent / Score / Product / Campaign Methods
953
+
954
+ ```typescript
955
+ setConsents(consents: ConsentInfo[]): this
956
+ setScores(scores: ScoreInfo[]): this
957
+ setProducts(products: ProductInfo[]): this
958
+ setMetadata(meta: Record<string, unknown>): this
959
+ setCampaign(info: CampaignInfo): this
960
+ setPlatform(info: PlatformInfo): this
961
+ setPushToken(token: string): this
962
+ ```
963
+
964
+ ### Event Methods
965
+
966
+ ```typescript
967
+ track(eventName: string, properties?: Record<string, unknown>): void
968
+ screen(screenName: string, properties?: Record<string, unknown>): void
969
+ async flush(): Promise<void>
970
+ ```
971
+
972
+ ### Notification Inbox Tracking Methods
973
+
974
+ ```typescript
975
+ async trackNotificationEvent(
976
+ trackingId: string,
977
+ eventType: 'delivered' | 'open' | 'click' | 'dismiss' | 'read',
978
+ options?: {
979
+ userAppId?: string;
980
+ userProfileId?: string;
981
+ deviceId?: string;
982
+ platform?: string;
983
+ buttonId?: string;
984
+ deeplink?: string;
985
+ currentScreen?: string;
986
+ metadata?: unknown[];
987
+ }
988
+ ): Promise<{ code?: number; message?: string } | undefined>
989
+
990
+ bindNotificationTracking(notiSdk: NotiSdkLike): () => void // trả về unsubscribe
991
+ ```
992
+
993
+ ### Session & Identity Methods
994
+
995
+ ```typescript
996
+ getUserIdentities(): UserIdentities
997
+ getProfileId(): string | null
998
+ getDeviceId(): string
999
+ getSessionId(): string
1000
+ getUserAttribute(name: keyof UserTraits): unknown
1001
+
1002
+ async destroy_session(): Promise<this>
1003
+ async reset(): Promise<this> // alias
1004
+ async destroy(): Promise<void> // cleanup timers + flush
1005
+ ```
1006
+
1007
+ ### Types
1008
+
1009
+ ```typescript
1010
+ interface UserTraits {
1011
+ full_name?: string;
1012
+ first_name?: string;
1013
+ last_name?: string;
1014
+ phone?: string; // 🔒 PII
1015
+ email?: string; // 🔒 PII
1016
+ gender?: 'M' | 'F' | 'O';
1017
+ dob?: string; // 'yyyy-MM-dd'
1018
+ idcard?: string; // 🔒 PII
1019
+ old_idcard?: string; // 🔒 PII
1020
+ address?: string; // 🔒 PII
1021
+ occupation?: string;
1022
+ nationality?: string;
1023
+ marital_status?: string;
1024
+ religion?: string; // 🔒 PII
1025
+ zalo_id?: string;
1026
+ tiktok_id?: string;
1027
+ mps_verified?: boolean;
1028
+ kyc_status?: string;
1029
+ [key: string]: unknown;
1030
+ }
1031
+
1032
+ interface ConsentInfo {
1033
+ consent_type: string;
1034
+ status: boolean;
1035
+ consent_date?: string; // 'yyyy-MM-dd'
1036
+ }
1037
+
1038
+ interface ScoreInfo {
1039
+ score_type: string;
1040
+ score_value: number;
1041
+ score_date?: string;
1042
+ }
1043
+
1044
+ interface ProductInfo {
1045
+ product_code: string;
1046
+ product_name?: string;
1047
+ status?: string;
1048
+ }
1049
+
1050
+ interface UserIdentities {
1051
+ user_id: string | null;
1052
+ profile_id: string | null;
1053
+ anonymous_id: string;
1054
+ device_id: string;
1055
+ session_id: string;
1056
+ }
1057
+
1058
+ interface ProfileResponse {
1059
+ code: number;
1060
+ message: string;
1061
+ transaction_id: string;
1062
+ profile_id: string;
1063
+ }
1064
+
1065
+ type NotificationEventType = 'delivered' | 'open' | 'click' | 'dismiss' | 'read';
1066
+
1067
+ interface NotificationTrackOptions {
1068
+ userAppId?: string;
1069
+ userProfileId?: string;
1070
+ deviceId?: string;
1071
+ platform?: string;
1072
+ buttonId?: string;
1073
+ deeplink?: string;
1074
+ currentScreen?: string;
1075
+ metadata?: unknown[];
1076
+ }
1077
+
1078
+ interface NotificationTrackResponse {
1079
+ code?: number;
1080
+ message?: string;
1081
+ [key: string]: unknown;
1082
+ }
1083
+
1084
+ /** Shape tối thiểu của message do react-native-noti-sdk trả về. */
1085
+ interface NotiMessageLike {
1086
+ id?: string;
1087
+ title?: string;
1088
+ body?: string;
1089
+ data?: Record<string, string>;
1090
+ }
1091
+
1092
+ /** Duck-typed — khớp với API react-native-noti-sdk, không cần import trực tiếp package đó. */
1093
+ interface NotiSdkLike {
1094
+ onMessage: (handler: (message: NotiMessageLike) => void) => () => void;
1095
+ onNotificationOpened: (handler: (message: NotiMessageLike) => void) => () => void;
1096
+ getInitialNotification?: () => Promise<NotiMessageLike | null>;
1097
+ }
1098
+ ```