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 +865 -319
- package/dist/CdpRnSdk.d.ts +15 -1
- package/dist/CdpRnSdk.d.ts.map +1 -1
- package/dist/CdpRnSdk.js +91 -1
- package/dist/CdpRnSdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +26 -0
- package/docs/getting-started.md +7 -5
- package/docs/notifications.md +130 -0
- package/package.json +14 -2
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
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
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
|
-
- [
|
|
22
|
-
- [
|
|
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
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
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
|
-
- [
|
|
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
|
|
49
|
+
npm install vc-cdp-rn-sdk
|
|
39
50
|
```
|
|
40
51
|
|
|
41
|
-
###
|
|
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
|
-
|
|
65
|
+
#### Android
|
|
48
66
|
|
|
49
|
-
Không cần
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
export async function
|
|
63
|
-
|
|
64
|
-
apiKey:
|
|
65
|
-
secretKey:
|
|
66
|
-
source:
|
|
67
|
-
serviceName: '
|
|
68
|
-
|
|
69
|
-
|
|
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 ý:**
|
|
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
|
-
│ -
|
|
90
|
-
│ - Load anonymous_id
|
|
91
|
-
│ -
|
|
92
|
-
│ -
|
|
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(
|
|
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
|
-
**
|
|
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
|
|
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(
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
211
|
+
### Trait Setters — Cập nhật từng thuộc tính
|
|
193
212
|
|
|
194
|
-
|
|
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(
|
|
223
|
+
.add_birthday('1990-01-15')
|
|
205
224
|
.add_idcard('012345678901')
|
|
206
225
|
.add_old_idcard('123456789')
|
|
207
|
-
.add_address('123
|
|
226
|
+
.add_address('123 Lê 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('
|
|
213
|
-
.add_tiktok_id('
|
|
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
|
-
|
|
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
|
|
234
|
-
| `add_mobile(v)` | `phone` | PII
|
|
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
|
|
238
|
-
| `add_old_idcard(v)` | `old_idcard` | PII
|
|
239
|
-
| `add_address(v)` | `address` | PII
|
|
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
|
|
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)` |
|
|
247
|
-
| `add_user_attributes(obj)` | nhiều trường | |
|
|
248
|
-
| `setUserAttributes(obj)` | nhiều trường |
|
|
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
|
-
###
|
|
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
|
-
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### Consents
|
|
255
280
|
|
|
256
281
|
```typescript
|
|
257
282
|
cdp.setConsents([
|
|
258
|
-
{
|
|
259
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
349
|
+
### destroy_session — Đăng xuất
|
|
348
350
|
|
|
349
351
|
```typescript
|
|
350
|
-
cdp.
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
367
|
+
## Event Tracking
|
|
362
368
|
|
|
363
|
-
|
|
369
|
+
### track — Custom event
|
|
364
370
|
|
|
365
371
|
```typescript
|
|
366
|
-
cdp.
|
|
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
|
-
###
|
|
390
|
+
### screen — Màn hình
|
|
372
391
|
|
|
373
|
-
|
|
392
|
+
Track lượt xem màn hình. Gửi event `screen_view`.
|
|
374
393
|
|
|
375
394
|
```typescript
|
|
376
|
-
|
|
395
|
+
cdp.screen('HomeScreen');
|
|
396
|
+
cdp.screen('LoanDetailScreen', { loan_code: 'CONSUMER_24M' });
|
|
397
|
+
cdp.screen('ProfileScreen', { tab: 'personal_info' });
|
|
398
|
+
```
|
|
377
399
|
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
445
|
+
### flush — Gửi ngay
|
|
390
446
|
|
|
391
|
-
|
|
447
|
+
```typescript
|
|
448
|
+
await cdp.flush();
|
|
449
|
+
```
|
|
392
450
|
|
|
393
451
|
```typescript
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
455
|
+
AppState.addEventListener('change', async (state) => {
|
|
456
|
+
if (state === 'background') {
|
|
457
|
+
await cdp.flush();
|
|
458
|
+
}
|
|
404
459
|
});
|
|
405
460
|
```
|
|
406
461
|
|
|
407
|
-
|
|
462
|
+
---
|
|
408
463
|
|
|
409
|
-
|
|
464
|
+
### destroy — Dọn dẹp
|
|
410
465
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 và xoá timer. Không xoá user data.
|
|
423
471
|
|
|
424
472
|
---
|
|
425
473
|
|
|
426
|
-
|
|
474
|
+
## Notification Inbox Tracking
|
|
427
475
|
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
+
**`AuthorizationStatus`**: `'authorized' | 'provisional' | 'denied' | 'notDetermined'`
|
|
445
578
|
|
|
446
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
//
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
640
|
+
`react-native-noti-sdk` **không có 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
|
-
|
|
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
|
-
|
|
688
|
+
**Headers:**
|
|
480
689
|
|
|
481
690
|
```
|
|
482
|
-
X-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
+
**Các trường PII bị mã hoá:**
|
|
488
716
|
|
|
489
|
-
|
|
717
|
+
| Trường | Mô 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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
+
### Best Practices
|
|
503
746
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
##
|
|
779
|
+
## Tích hợp React Navigation đầy đủ
|
|
513
780
|
|
|
514
|
-
|
|
781
|
+
Setup hoàn chỉnh với auto screen tracking và AppState flush:
|
|
515
782
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
##
|
|
884
|
+
## API Reference
|
|
544
885
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
```
|