react-native-timacare 3.3.64 → 3.3.65

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.
Files changed (61) hide show
  1. package/lib/commonjs/components/AlertLoan.js +1 -1
  2. package/lib/commonjs/components/AlertLoan.js.flow +11 -2
  3. package/lib/commonjs/components/AlertLoan.js.map +1 -1
  4. package/lib/commonjs/components/ItemTimaOne.js +1 -1
  5. package/lib/commonjs/components/ItemTimaOne.js.flow +2 -0
  6. package/lib/commonjs/components/ItemTimaOne.js.map +1 -1
  7. package/lib/commonjs/navigation/primary-navigator.js +1 -1
  8. package/lib/commonjs/navigation/primary-navigator.js.flow +9 -0
  9. package/lib/commonjs/navigation/primary-navigator.js.map +1 -1
  10. package/lib/commonjs/screens/full-submit/selfie.js +1 -1
  11. package/lib/commonjs/screens/full-submit/selfie.js.flow +257 -114
  12. package/lib/commonjs/screens/full-submit/selfie.js.map +1 -1
  13. package/lib/commonjs/screens/selfie/SelfieCamera.js +2 -0
  14. package/lib/commonjs/screens/selfie/SelfieCamera.js.flow +302 -0
  15. package/lib/commonjs/screens/selfie/SelfieCamera.js.map +1 -0
  16. package/lib/commonjs/screens/selfie/index.js +1 -1
  17. package/lib/commonjs/screens/selfie/index.js.flow +295 -165
  18. package/lib/commonjs/screens/selfie/index.js.map +1 -1
  19. package/lib/commonjs/screens/v3/ReviewInfoLoanBuyCar.js +2 -0
  20. package/lib/commonjs/screens/v3/ReviewInfoLoanBuyCar.js.flow +521 -0
  21. package/lib/commonjs/screens/v3/ReviewInfoLoanBuyCar.js.map +1 -0
  22. package/lib/commonjs/services/api/api.js +1 -1
  23. package/lib/commonjs/services/api/api.js.flow +41 -7
  24. package/lib/commonjs/services/api/api.js.map +1 -1
  25. package/lib/module/components/AlertLoan.js +1 -1
  26. package/lib/module/components/AlertLoan.js.map +1 -1
  27. package/lib/module/components/ItemTimaOne.js +1 -1
  28. package/lib/module/components/ItemTimaOne.js.map +1 -1
  29. package/lib/module/navigation/primary-navigator.js +1 -1
  30. package/lib/module/navigation/primary-navigator.js.map +1 -1
  31. package/lib/module/screens/full-submit/selfie.js +1 -1
  32. package/lib/module/screens/full-submit/selfie.js.map +1 -1
  33. package/lib/module/screens/selfie/SelfieCamera.js +2 -0
  34. package/lib/module/screens/selfie/SelfieCamera.js.map +1 -0
  35. package/lib/module/screens/selfie/index.js +1 -1
  36. package/lib/module/screens/selfie/index.js.map +1 -1
  37. package/lib/module/screens/v3/ReviewInfoLoanBuyCar.js +2 -0
  38. package/lib/module/screens/v3/ReviewInfoLoanBuyCar.js.map +1 -0
  39. package/lib/module/services/api/api.js +1 -1
  40. package/lib/module/services/api/api.js.map +1 -1
  41. package/lib/typescript/components/AlertLoan.d.ts.map +1 -1
  42. package/lib/typescript/components/ItemTimaOne.d.ts.map +1 -1
  43. package/lib/typescript/navigation/primary-navigator.d.ts +2 -0
  44. package/lib/typescript/navigation/primary-navigator.d.ts.map +1 -1
  45. package/lib/typescript/screens/full-submit/selfie.d.ts.map +1 -1
  46. package/lib/typescript/screens/selfie/SelfieCamera.d.ts +2 -0
  47. package/lib/typescript/screens/selfie/SelfieCamera.d.ts.map +1 -0
  48. package/lib/typescript/screens/selfie/index.d.ts.map +1 -1
  49. package/lib/typescript/screens/v3/ReviewInfoLoanBuyCar.d.ts +2 -0
  50. package/lib/typescript/screens/v3/ReviewInfoLoanBuyCar.d.ts.map +1 -0
  51. package/lib/typescript/services/api/api.d.ts +32 -0
  52. package/lib/typescript/services/api/api.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/AlertLoan.tsx +11 -2
  55. package/src/components/ItemTimaOne.tsx +2 -0
  56. package/src/navigation/primary-navigator.tsx +9 -0
  57. package/src/screens/full-submit/selfie.tsx +257 -114
  58. package/src/screens/selfie/SelfieCamera.tsx +302 -0
  59. package/src/screens/selfie/index.tsx +295 -165
  60. package/src/screens/v3/ReviewInfoLoanBuyCar.tsx +521 -0
  61. package/src/services/api/api.ts +41 -7
@@ -1,33 +1,26 @@
1
1
  //@ts-nocheck
2
- import React, { useState, Fragment } from 'react';
2
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import {
4
4
  ActivityIndicator,
5
- Dimensions,
5
+ Animated,
6
+ Easing,
6
7
  Image,
7
- ImageStyle,
8
- Platform,
9
8
  SafeAreaView,
10
9
  ScrollView,
11
- TextStyle,
10
+ StyleSheet,
12
11
  View,
13
- ViewStyle,
14
12
  } from 'react-native';
15
- import {
16
- CommonActions,
17
- useNavigation,
18
- StackActions,
19
- } from '@react-navigation/native';
20
- import { observer, Observer } from 'mobx-react-lite';
13
+ import { useNavigation, StackActions } from '@react-navigation/native';
14
+ import { observer } from 'mobx-react-lite';
15
+ import LinearGradient from 'react-native-linear-gradient';
21
16
  import { commonStyles } from '../CommonStyles';
22
- import KeyboardSpacer from '../../components/keyboardspace';
23
17
  import MButton from '../../components/MButton';
24
- import { color } from '../../theme';
25
18
  import { MText } from '../../components/MText';
26
19
  import {
27
20
  CIMBBank,
28
- IconBack,
29
- IconBackWhite,
30
- IconCamera,
21
+ IConCamera,
22
+ Tick2,
23
+ TimaSmall,
31
24
  TimaSmallWhite,
32
25
  } from '../../assets/icons';
33
26
  import { ScreenNames } from '../../navigation';
@@ -35,36 +28,55 @@ import appStore from '../../AppStore';
35
28
  import selfieStore from './SelfieStore';
36
29
  import nationalIDStore from '../nationalID/Store';
37
30
  import Loading from '../../components/Loading';
38
- import LinearGradient from 'react-native-linear-gradient';
39
- import livenessStore from '../liveness/LivenessStore';
40
31
  import MHeader from '../../components/MHeader';
41
32
 
42
- const logoView: ImageStyle = {
43
- position: 'absolute',
44
- width: '100%',
45
- height: '100%',
46
- resizeMode: 'cover',
47
- };
33
+ // require() ở module scope: không phải resolve lại asset mỗi lần render
34
+ const BACKGROUND = require('../../assets/img/EKYC.png');
35
+ const PLACEHOLDER = require('../../assets/img/selfie.png');
36
+
37
+ // Khung dọc 3:4 - đúng tỉ lệ ảnh máy ảnh trước trả về nên ảnh khách chụp
38
+ // không bị cắt, và khớp với khung ở màn CimbSelfie
39
+ const PREVIEW_ASPECT = 3 / 4;
40
+
41
+ const NOTES = [
42
+ 'Nhìn thẳng vào máy ảnh, khuôn mặt nằm gọn trong khung hình',
43
+ 'Chụp nơi đủ sáng, tránh ngược sáng hoặc bóng đổ lên mặt',
44
+ 'Không đeo kính, khẩu trang, mũ và để lộ rõ đường nét khuôn mặt',
45
+ ];
48
46
 
49
47
  export const Selfie = observer(function Selfie(props: any) {
50
48
  const navigation = useNavigation();
51
- const step = 3;
52
49
  const [isLoading, setIsLoading] = useState(false);
53
50
  const [photo, setPhoto] = useState(null);
54
- const [base64, setBase64] = useState(null);
51
+ // Chặn double-submit: setIsLoading là bất đồng bộ nên hai lần bấm liên tiếp
52
+ // vẫn lọt qua được nếu chỉ dựa vào state.
53
+ const submitting = useRef(false);
55
54
  const loan = props.route.params?.loan;
56
55
 
57
- const nextStep = (values) => {
58
- const form = new FormData();
56
+ // Ảnh vừa chụp hiện lên bằng fade + scale nhẹ để người dùng thấy rõ là khung
57
+ // ảnh đã đổi nội dung (trước đây ảnh thay thầm lặng, dễ tưởng chưa chụp được)
58
+ const reveal = useRef(new Animated.Value(1)).current;
59
+ useEffect(() => {
60
+ if (!photo) return;
61
+ reveal.setValue(0);
62
+ Animated.timing(reveal, {
63
+ toValue: 1,
64
+ duration: 260,
65
+ easing: Easing.out(Easing.cubic),
66
+ useNativeDriver: true,
67
+ }).start();
68
+ }, [photo, reveal]);
69
+
70
+ const nextStep = useCallback(() => {
71
+ if (!photo || submitting.current) return;
72
+ submitting.current = true;
59
73
  setIsLoading(true);
60
- form.append('images', {
61
- uri: photo,
62
- type: 'image/jpg',
63
- name: 'image.jpg',
64
- });
65
- form.append('loanbriefId', loan.id);
66
- form.append('TypeDocumentId', 125);
67
- form.append('UploadFrom', 3);
74
+
75
+ const stopLoading = () => {
76
+ submitting.current = false;
77
+ setIsLoading(false);
78
+ };
79
+
68
80
  const formSelfie = new FormData();
69
81
  formSelfie.append('NationalCard', {
70
82
  uri: nationalIDStore.photo,
@@ -81,171 +93,289 @@ export const Selfie = observer(function Selfie(props: any) {
81
93
  formSelfie,
82
94
  loan.id,
83
95
  () => {
96
+ // Chỉ dựng form upload khi ảnh đã qua bước đối chiếu
97
+ const form = new FormData();
98
+ form.append('images', {
99
+ uri: photo,
100
+ type: 'image/jpg',
101
+ name: 'image.jpg',
102
+ });
103
+ form.append('loanbriefId', loan.id);
104
+ form.append('TypeDocumentId', 125);
105
+ form.append('UploadFrom', 3);
106
+
84
107
  selfieStore.uploadImage(
85
108
  form,
86
109
  () => {
87
- setIsLoading(false);
88
- livenessStore.getConfigLiveness((data) => {
89
- if (data === 1) {
90
- navigation.dispatch(
91
- StackActions.push(ScreenNames.LiveNess, { loan })
92
- );
93
- } else {
94
- navigation.dispatch(
95
- StackActions.push(ScreenNames.LivenessV2, { loan })
96
- );
97
- }
98
- });
110
+ stopLoading();
111
+ navigation.dispatch(
112
+ StackActions.push(ScreenNames.LivenessV2, { loan })
113
+ );
99
114
  },
100
- () => {
101
- setIsLoading(false);
102
- }
115
+ stopLoading
103
116
  );
104
117
  },
105
- () => {
106
- setIsLoading(false);
107
- }
118
+ stopLoading
108
119
  );
109
- };
120
+ }, [photo, loan, navigation]);
110
121
 
111
- const takePhoto = () => {
112
- //@ts-ignore
113
- navigation.push(ScreenNames.Camera, {
122
+ const takePhoto = useCallback(() => {
123
+ navigation.push(ScreenNames.SelfieCamera, {
114
124
  front: true,
115
125
  selfie: true,
116
- callback: (photo) => {
117
- selfieStore.photo = photo;
118
- setPhoto(photo);
126
+ callback: (uri) => {
127
+ selfieStore.photo = uri;
128
+ setPhoto(uri);
119
129
  },
120
130
  base64: (data) => {
121
- setBase64(data);
122
131
  selfieStore.photoBase64 = data;
123
132
  },
124
133
  });
125
- };
134
+ }, [navigation]);
135
+
136
+ const goBack = useCallback(() => navigation.goBack(), [navigation]);
137
+
138
+ const capturing = appStore.isLoading || selfieStore.isLoading;
139
+ const canSubmit = !!photo && !isLoading;
126
140
 
127
141
  return (
128
- <View style={{ flex: 1 }}>
129
- <Image source={require('../../assets/img/EKYC.png')} style={logoView} />
130
- <MHeader title="Xác thực khách hàng" onBack={() => navigation.goBack()} />
142
+ <View style={commonStyles.fill}>
143
+ <Image source={BACKGROUND} style={styles.background} />
144
+ <MHeader title="Xác thực khách hàng" onBack={goBack} />
131
145
  <SafeAreaView style={commonStyles.fill}>
132
- <ScrollView style={commonStyles.fill}>
133
- <View style={{ paddingHorizontal: 16, paddingVertical: 16 }}>
134
- <View
135
- style={{
136
- flexDirection: 'row',
137
- alignItems: 'center',
138
- marginTop: 16,
139
- }}
140
- >
141
- <TimaSmallWhite />
142
- <MText
146
+ <ScrollView
147
+ style={commonStyles.fill}
148
+ contentContainerStyle={styles.scrollContent}
149
+ showsVerticalScrollIndicator={false}
150
+ >
151
+ <View style={styles.titleRow}>
152
+ <TimaSmallWhite />
153
+ <MText style={styles.title}>Ảnh chụp khuôn mặt</MText>
154
+ </View>
155
+
156
+ <View style={styles.card}>
157
+ {/* Bọc một lớp cắt riêng: để Image tự bo góc thì mép ảnh vẫn thò ra
158
+ ngoài đường cong của card trên Android */}
159
+ <View style={styles.previewFrame}>
160
+ <Animated.Image
161
+ source={photo ? { uri: photo } : PLACEHOLDER}
162
+ resizeMode="cover"
143
163
  style={[
144
- commonStyles.textNormalBold,
145
- { fontSize: 16, color: '#FFFFFF', marginLeft: 8 },
164
+ styles.preview,
165
+ {
166
+ opacity: reveal,
167
+ transform: [
168
+ {
169
+ scale: reveal.interpolate({
170
+ inputRange: [0, 1],
171
+ outputRange: [0.97, 1],
172
+ }),
173
+ },
174
+ ],
175
+ },
146
176
  ]}
147
- >
148
- Ảnh chụp khuôn mặt
177
+ />
178
+ </View>
179
+ {/* Nhãn trạng thái: phân biệt rõ ảnh mẫu minh hoạ và ảnh đã chụp */}
180
+ <View style={[styles.badge, photo && styles.badgeDone]}>
181
+ {photo ? <Tick2 /> : null}
182
+ <MText style={[styles.badgeText, photo && styles.badgeTextDone]}>
183
+ {photo ? 'Ảnh của bạn' : 'Ảnh mẫu'}
149
184
  </MText>
150
185
  </View>
151
- <Image
152
- source={
153
- photo ? { uri: photo } : require('../../assets/img/selfie.png')
154
- }
155
- style={{
156
- width: Dimensions.get('window').width - 32,
157
- height: ((Dimensions.get('window').width - 32) * 534) / 564,
158
- borderWidth: 8,
159
- borderRadius: 6,
160
- borderColor: '#FFFFFF',
161
- marginTop: 16,
162
- }}
163
- />
186
+ </View>
187
+
188
+ <View style={styles.noteCard}>
189
+ <MText style={styles.noteTitle}>Lưu ý khi chụp ảnh khuôn mặt</MText>
190
+ {NOTES.map((note) => (
191
+ <View key={note} style={styles.noteRow}>
192
+ <TimaSmall style={styles.noteBullet} />
193
+ <MText style={styles.noteText}>{note}</MText>
194
+ </View>
195
+ ))}
164
196
  </View>
165
197
  </ScrollView>
166
- <View
167
- style={{
168
- flexDirection: 'row',
169
- paddingHorizontal: 16,
170
- marginBottom: 30,
171
- }}
172
- >
198
+
199
+ <View style={styles.footer}>
173
200
  <MButton
174
- //@ts-ignore
175
- onPress={() => {
176
- takePhoto();
177
- }}
178
- disabled={appStore.isLoading || selfieStore.isLoading}
179
- style={[
180
- commonStyles.row,
181
- commonStyles.alignCenter,
182
- commonStyles.justifyCenter,
183
- {
184
- backgroundColor: '#FFFFFF',
185
- height: 40,
186
- borderRadius: 30,
187
- flex: 1,
188
- marginRight: 8,
189
- borderWidth: 1,
190
- borderColor: '#EF4123',
191
- },
192
- ]}
201
+ onPress={takePhoto}
202
+ disabled={capturing}
203
+ style={[styles.captureButton, capturing && styles.buttonMuted]}
193
204
  >
194
- <MText style={[commonStyles.textNormal, { color: '#F05123' }]}>
195
- Chụp ảnh
205
+ <IConCamera width={16} height={16} />
206
+ <MText style={styles.captureText}>
207
+ {photo ? 'Chụp lại' : 'Chụp ảnh'}
196
208
  </MText>
197
209
  </MButton>
198
- <Observer>
199
- {() => (
200
- <MButton
201
- disabled={!photo || isLoading}
202
- onPress={nextStep}
203
- style={{ flex: 1 }}
210
+
211
+ <MButton
212
+ disabled={!canSubmit}
213
+ onPress={nextStep}
214
+ style={commonStyles.fill}
215
+ >
216
+ {canSubmit ? (
217
+ <LinearGradient
218
+ colors={['#FF7A00', '#EF4123']}
219
+ start={{ x: 0, y: 0 }}
220
+ end={{ x: 1, y: 1 }}
221
+ style={[styles.submitButton, styles.submitShadow]}
204
222
  >
205
- <LinearGradient
206
- colors={['#FF7A00', '#EF4123']}
207
- style={[
208
- commonStyles.alignCenter,
209
- commonStyles.justifyCenter,
210
- {
211
- height: 40,
212
- borderRadius: 30,
213
- marginLeft: 8,
214
- },
215
- ]}
216
- >
217
- {isLoading ? (
218
- <ActivityIndicator color={'white'} size="small" />
219
- ) : (
220
- <MText
221
- style={[commonStyles.textNormal, { color: 'white' }]}
222
- >
223
- Tiếp tục
224
- </MText>
225
- )}
226
- </LinearGradient>
227
- </MButton>
223
+ <MText style={styles.submitText}>Tiếp tục</MText>
224
+ </LinearGradient>
225
+ ) : (
226
+ // Trạng thái chưa chụp/đang xử lý phải nhìn là biết chưa bấm được
227
+ <View style={[styles.submitButton, styles.submitDisabled]}>
228
+ {isLoading ? (
229
+ <ActivityIndicator color={'#FFFFFF'} size="small" />
230
+ ) : (
231
+ <MText style={styles.submitTextDisabled}>Tiếp tục</MText>
232
+ )}
233
+ </View>
228
234
  )}
229
- </Observer>
235
+ </MButton>
230
236
  </View>
237
+
231
238
  {loan?.isPipelineCIMB && (
232
- <View
233
- style={{
234
- marginBottom: 24,
235
- marginTop: 8,
236
- flexDirection: 'row',
237
- justifyContent: 'center',
238
- }}
239
- >
240
- <MText style={{ fontSize: 12, color: '#333333' }}>
241
- Cung cấp bởi{' '}
242
- </MText>
239
+ <View style={styles.cimbRow}>
240
+ <MText style={styles.cimbText}>Cung cấp bởi </MText>
243
241
  <CIMBBank />
244
242
  </View>
245
243
  )}
246
- <KeyboardSpacer />
247
244
  </SafeAreaView>
248
245
  <Loading isLoading={isLoading} />
249
246
  </View>
250
247
  );
251
248
  });
249
+
250
+ const styles = StyleSheet.create({
251
+ background: {
252
+ position: 'absolute',
253
+ width: '100%',
254
+ height: '100%',
255
+ resizeMode: 'cover',
256
+ },
257
+ scrollContent: { paddingHorizontal: 24, paddingBottom: 24 },
258
+
259
+ titleRow: { flexDirection: 'row', alignItems: 'center', marginTop: 20 },
260
+ title: {
261
+ ...commonStyles.textNormalBold,
262
+ fontSize: 16,
263
+ color: '#FFFFFF',
264
+ marginLeft: 8,
265
+ letterSpacing: 0.2,
266
+ },
267
+
268
+ card: {
269
+ marginTop: 16,
270
+ padding: 8,
271
+ borderRadius: 20,
272
+ backgroundColor: '#FFFFFF',
273
+ shadowColor: '#7A2E12',
274
+ shadowOffset: { width: 0, height: 10 },
275
+ shadowOpacity: 0.18,
276
+ shadowRadius: 20,
277
+ elevation: 8,
278
+ },
279
+ // Bán kính trong = bán kính card (20) - padding (8) để hai đường cong khớp nhau
280
+ previewFrame: {
281
+ width: '100%',
282
+ aspectRatio: PREVIEW_ASPECT,
283
+ borderRadius: 12,
284
+ overflow: 'hidden',
285
+ backgroundColor: '#F5EDE9',
286
+ },
287
+ preview: { width: '100%', height: '100%' },
288
+ badge: {
289
+ position: 'absolute',
290
+ left: 20,
291
+ bottom: 20,
292
+ flexDirection: 'row',
293
+ alignItems: 'center',
294
+ paddingHorizontal: 10,
295
+ paddingVertical: 5,
296
+ borderRadius: 14,
297
+ backgroundColor: 'rgba(0, 0, 0, 0.45)',
298
+ },
299
+ badgeDone: { backgroundColor: 'rgba(255, 255, 255, 0.94)' },
300
+ badgeText: { color: '#FFFFFF', fontSize: 11, fontWeight: '600' },
301
+ badgeTextDone: { color: '#28A745', marginLeft: 6 },
302
+
303
+ noteCard: {
304
+ marginTop: 20,
305
+ padding: 16,
306
+ borderRadius: 16,
307
+ backgroundColor: 'rgba(255, 255, 255, 0.75)',
308
+ borderWidth: 1,
309
+ borderColor: '#FFE1D3',
310
+ },
311
+ noteTitle: {
312
+ ...commonStyles.textNormalBold,
313
+ fontSize: 15,
314
+ color: '#333333',
315
+ },
316
+ noteRow: { flexDirection: 'row', marginTop: 12 },
317
+ noteBullet: { marginTop: 5 },
318
+ noteText: {
319
+ color: '#5A5A5A',
320
+ marginLeft: 10,
321
+ fontSize: 13,
322
+ lineHeight: 20,
323
+ flex: 1,
324
+ },
325
+
326
+ // SafeAreaView đã cộng inset dưới trên iOS nên chỉ cần khoảng thở cố định
327
+ footer: {
328
+ flexDirection: 'row',
329
+ paddingHorizontal: 24,
330
+ paddingTop: 4,
331
+ marginBottom: 24,
332
+ },
333
+ captureButton: {
334
+ flexDirection: 'row',
335
+ alignItems: 'center',
336
+ justifyContent: 'center',
337
+ backgroundColor: '#FFFFFF',
338
+ height: 48,
339
+ borderRadius: 24,
340
+ flex: 1,
341
+ marginRight: 10,
342
+ borderWidth: 1.5,
343
+ borderColor: '#EF4123',
344
+ },
345
+ buttonMuted: { opacity: 0.5 },
346
+ captureText: {
347
+ ...commonStyles.textNormal,
348
+ color: '#F05123',
349
+ fontWeight: '600',
350
+ marginLeft: 8,
351
+ },
352
+ submitButton: {
353
+ alignItems: 'center',
354
+ justifyContent: 'center',
355
+ height: 48,
356
+ borderRadius: 24,
357
+ },
358
+ submitShadow: {
359
+ shadowColor: '#EF4123',
360
+ shadowOffset: { width: 0, height: 6 },
361
+ shadowOpacity: 0.35,
362
+ shadowRadius: 12,
363
+ elevation: 6,
364
+ },
365
+ submitDisabled: { backgroundColor: '#D8D2CF' },
366
+ submitText: {
367
+ ...commonStyles.textNormal,
368
+ color: '#FFFFFF',
369
+ fontWeight: '600',
370
+ },
371
+ submitTextDisabled: { ...commonStyles.textNormal, color: '#FFFFFF' },
372
+
373
+ cimbRow: {
374
+ marginTop: -8,
375
+ marginBottom: 24,
376
+ flexDirection: 'row',
377
+ justifyContent: 'center',
378
+ alignItems: 'center',
379
+ },
380
+ cimbText: { fontSize: 12, color: '#333333' },
381
+ });