@kernaq/verify-react-native 2.0.0 → 2.0.1
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/dist/index.d.ts +95 -50
- package/dist/index.esm.js +1121 -571
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1119 -567
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/KernaqVerify.tsx +144 -175
- package/src/index.ts +9 -4
- package/src/locale.ts +32 -29
- package/src/quality.ts +180 -0
- package/src/steps/DocumentStep.tsx +214 -77
- package/src/steps/IntroStep.tsx +58 -37
- package/src/steps/LivenessStep.tsx +218 -125
- package/src/steps/ProcessingStep.tsx +10 -34
- package/src/steps/ResultStep.tsx +99 -35
- package/src/steps/SelfieStep.tsx +165 -96
- package/src/theme.ts +22 -14
- package/src/types.ts +27 -51
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LivenessStep — randomised action challenges shown as an overlay on the camera feed.
|
|
3
|
+
* Tasks: blink twice, turn left, turn right, nod, open mouth.
|
|
4
|
+
* Instruction is overlaid at the bottom of the viewport (mirrors web v2).
|
|
5
|
+
*/
|
|
6
|
+
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
|
2
7
|
import {
|
|
3
8
|
View,
|
|
4
9
|
Text,
|
|
@@ -7,114 +12,209 @@ import {
|
|
|
7
12
|
ActivityIndicator,
|
|
8
13
|
Animated,
|
|
9
14
|
} from 'react-native'
|
|
10
|
-
import { Camera } from 'react-native-vision-camera'
|
|
15
|
+
import { Camera, useCameraDevice } from 'react-native-vision-camera'
|
|
11
16
|
import { useCamera } from '../hooks/useCamera'
|
|
12
17
|
import type { ResolvedTheme } from '../theme'
|
|
13
|
-
import type { VerifyLocale } from '../types'
|
|
18
|
+
import type { VerifyLocale, LivenessTask } from '../types'
|
|
19
|
+
|
|
20
|
+
interface TaskDef {
|
|
21
|
+
id: LivenessTask
|
|
22
|
+
label: string
|
|
23
|
+
duration: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TASK_POOL: TaskDef[] = [
|
|
27
|
+
{ id: 'blink', label: 'Blink twice', duration: 3000 },
|
|
28
|
+
{ id: 'turn-left', label: 'Turn your head left', duration: 3000 },
|
|
29
|
+
{ id: 'turn-right', label: 'Turn your head right', duration: 3000 },
|
|
30
|
+
{ id: 'nod', label: 'Nod your head', duration: 3000 },
|
|
31
|
+
{ id: 'open-mouth', label: 'Open your mouth', duration: 3000 },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function shuffle<T>(arr: T[]): T[] {
|
|
35
|
+
const a = [...arr]
|
|
36
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
37
|
+
const j = Math.floor(Math.random() * (i + 1))
|
|
38
|
+
;[a[i], a[j]] = [a[j]!, a[i]!]
|
|
39
|
+
}
|
|
40
|
+
return a
|
|
41
|
+
}
|
|
14
42
|
|
|
15
43
|
interface Props {
|
|
16
|
-
theme:
|
|
17
|
-
locale:
|
|
18
|
-
duration:
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
theme: ResolvedTheme
|
|
45
|
+
locale: VerifyLocale
|
|
46
|
+
duration: number
|
|
47
|
+
taskCount?: number
|
|
48
|
+
onCapture: (uri: string) => void
|
|
21
49
|
}
|
|
22
50
|
|
|
23
|
-
export function LivenessStep({ theme, locale, duration,
|
|
24
|
-
const { cameraRef,
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
51
|
+
export function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture }: Props) {
|
|
52
|
+
const { cameraRef, startRecording, stopRecording } = useCamera('front')
|
|
53
|
+
const device = useCameraDevice('front')
|
|
54
|
+
|
|
55
|
+
const tasks = useMemo(() => shuffle(TASK_POOL).slice(0, Math.min(taskCount, TASK_POOL.length)), [taskCount])
|
|
56
|
+
|
|
57
|
+
const [taskIdx, setTaskIdx] = useState(0)
|
|
58
|
+
const [started, setStarted] = useState(false) // task in progress
|
|
59
|
+
const [taskDone, setTaskDone] = useState(0) // completed tasks count
|
|
60
|
+
const [progress, setProgress] = useState(0) // 0–100 for current task bar
|
|
61
|
+
const [complete, setComplete] = useState(false)
|
|
62
|
+
const [recording, setRecording] = useState(false)
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
const progressTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
65
|
+
const taskTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
66
|
+
const pulseAnim = useRef(new Animated.Value(1)).current
|
|
67
|
+
|
|
68
|
+
// Pulse REC dot
|
|
32
69
|
useEffect(() => {
|
|
33
70
|
if (recording) {
|
|
34
71
|
Animated.loop(
|
|
35
72
|
Animated.sequence([
|
|
36
|
-
Animated.timing(pulseAnim, { toValue: 0.
|
|
37
|
-
Animated.timing(pulseAnim, { toValue: 1,
|
|
73
|
+
Animated.timing(pulseAnim, { toValue: 0.2, duration: 600, useNativeDriver: true }),
|
|
74
|
+
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
|
|
38
75
|
])
|
|
39
76
|
).start()
|
|
40
77
|
} else {
|
|
78
|
+
pulseAnim.stopAnimation()
|
|
41
79
|
pulseAnim.setValue(1)
|
|
42
80
|
}
|
|
43
81
|
}, [recording, pulseAnim])
|
|
44
82
|
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
const startTask = () => {
|
|
84
|
+
if (!recording) {
|
|
85
|
+
// Begin recording when first task starts
|
|
86
|
+
startRecording(
|
|
87
|
+
(video) => { onCapture(video.uri) },
|
|
88
|
+
() => {}
|
|
89
|
+
)
|
|
90
|
+
setRecording(true)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const task = tasks[taskIdx]
|
|
94
|
+
if (!task) return
|
|
95
|
+
setStarted(true)
|
|
96
|
+
setProgress(0)
|
|
97
|
+
|
|
98
|
+
const start = Date.now()
|
|
99
|
+
progressTimer.current = setInterval(() => {
|
|
100
|
+
const elapsed = Date.now() - start
|
|
101
|
+
const pct = Math.min(100, (elapsed / task.duration) * 100)
|
|
102
|
+
setProgress(pct)
|
|
103
|
+
if (elapsed >= task.duration) {
|
|
104
|
+
clearInterval(progressTimer.current!)
|
|
105
|
+
progressTimer.current = null
|
|
106
|
+
nextTask()
|
|
66
107
|
}
|
|
67
|
-
},
|
|
108
|
+
}, 80)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nextTask = () => {
|
|
112
|
+
const next = taskIdx + 1
|
|
113
|
+
setTaskDone(prev => prev + 1)
|
|
114
|
+
if (next >= tasks.length) {
|
|
115
|
+
finishLiveness()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
setTaskIdx(next)
|
|
119
|
+
setStarted(false)
|
|
120
|
+
setProgress(0)
|
|
121
|
+
// Short pause then auto-start next task
|
|
122
|
+
taskTimer.current = setTimeout(() => startTask(), 500)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const finishLiveness = () => {
|
|
126
|
+
setComplete(true)
|
|
127
|
+
taskTimer.current = setTimeout(() => {
|
|
128
|
+
stopRecording()
|
|
129
|
+
}, 1000)
|
|
68
130
|
}
|
|
69
131
|
|
|
70
132
|
useEffect(() => {
|
|
71
133
|
return () => {
|
|
72
|
-
if (
|
|
134
|
+
if (progressTimer.current) clearInterval(progressTimer.current)
|
|
135
|
+
if (taskTimer.current) clearTimeout(taskTimer.current)
|
|
73
136
|
}
|
|
74
137
|
}, [])
|
|
75
138
|
|
|
76
139
|
if (!device) return <ActivityIndicator color={theme.accent} style={{ margin: 40 }} />
|
|
77
140
|
|
|
141
|
+
const currentTask = tasks[taskIdx]
|
|
142
|
+
const s = styles(theme)
|
|
143
|
+
|
|
78
144
|
return (
|
|
79
145
|
<View style={s.container}>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
</
|
|
146
|
+
<View style={s.heading}>
|
|
147
|
+
<Text style={s.title}>{complete ? locale.liveness_complete : locale.liveness_title}</Text>
|
|
148
|
+
{!complete && <Text style={s.subtitle}>{locale.liveness_ready}</Text>}
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
{/* Task progress dots */}
|
|
152
|
+
{!complete && (
|
|
153
|
+
<View style={s.taskDots}>
|
|
154
|
+
{tasks.map((_, i) => (
|
|
155
|
+
<View
|
|
156
|
+
key={i}
|
|
157
|
+
style={[
|
|
158
|
+
s.dot,
|
|
159
|
+
i < taskDone && s.dotDone,
|
|
160
|
+
i === taskIdx && s.dotActive,
|
|
161
|
+
]}
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</View>
|
|
165
|
+
)}
|
|
84
166
|
|
|
167
|
+
{/* Camera with in-viewport overlay */}
|
|
85
168
|
<View style={s.viewport}>
|
|
86
169
|
<Camera
|
|
87
170
|
ref={cameraRef}
|
|
88
171
|
style={StyleSheet.absoluteFill}
|
|
89
172
|
device={device}
|
|
90
|
-
isActive
|
|
91
|
-
video
|
|
92
|
-
audio={
|
|
173
|
+
isActive
|
|
174
|
+
video
|
|
175
|
+
audio={false}
|
|
93
176
|
/>
|
|
177
|
+
|
|
94
178
|
<View style={s.ovalGuide} pointerEvents="none" />
|
|
95
179
|
|
|
180
|
+
{/* Recording badge */}
|
|
96
181
|
{recording && (
|
|
97
|
-
|
|
98
|
-
<View style={s.
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
<View style={s.recBadge}>
|
|
183
|
+
<Animated.View style={[s.recDot, { opacity: pulseAnim }]} />
|
|
184
|
+
<Text style={s.recText}>REC</Text>
|
|
185
|
+
</View>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Instruction overlay — always visible on video */}
|
|
189
|
+
{currentTask && !complete && (
|
|
190
|
+
<View style={s.overlay}>
|
|
191
|
+
<View style={s.overlayPill}>
|
|
192
|
+
<Text style={s.overlayNum}>{taskIdx + 1}/{tasks.length}</Text>
|
|
193
|
+
<Text style={s.overlayLabel}>{currentTask.label}</Text>
|
|
101
194
|
</View>
|
|
102
|
-
|
|
103
|
-
<
|
|
195
|
+
{started && (
|
|
196
|
+
<View style={s.barWrap}>
|
|
197
|
+
<View style={[s.barFill, { width: `${progress}%` }]} />
|
|
198
|
+
</View>
|
|
199
|
+
)}
|
|
200
|
+
</View>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{complete && (
|
|
204
|
+
<View style={s.overlay}>
|
|
205
|
+
<View style={s.overlayPill}>
|
|
206
|
+
<Text style={s.overlayLabel}>{locale.liveness_complete}</Text>
|
|
104
207
|
</View>
|
|
105
|
-
|
|
208
|
+
</View>
|
|
106
209
|
)}
|
|
107
210
|
</View>
|
|
108
211
|
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<Text style={s.btnSecondaryText}>{locale.selfie_retake_btn}</Text>
|
|
116
|
-
</TouchableOpacity>
|
|
117
|
-
</>
|
|
212
|
+
{/* Begin button — only shown before first task starts */}
|
|
213
|
+
{!started && !complete && currentTask && taskDone === 0 && (
|
|
214
|
+
<TouchableOpacity style={s.btn} onPress={startTask} activeOpacity={0.85}>
|
|
215
|
+
<Text style={s.btnText}>Begin</Text>
|
|
216
|
+
<Text style={s.btnArrow}> →</Text>
|
|
217
|
+
</TouchableOpacity>
|
|
118
218
|
)}
|
|
119
219
|
</View>
|
|
120
220
|
)
|
|
@@ -122,76 +222,69 @@ export function LivenessStep({ theme, locale, duration, onCapture, onBack }: Pro
|
|
|
122
222
|
|
|
123
223
|
const styles = (t: ResolvedTheme) =>
|
|
124
224
|
StyleSheet.create({
|
|
125
|
-
container: { padding:
|
|
126
|
-
|
|
127
|
-
|
|
225
|
+
container: { flex: 1, padding: 20, gap: 12 },
|
|
226
|
+
heading: { gap: 4, alignItems: 'center' },
|
|
227
|
+
title: {
|
|
228
|
+
fontSize: 20, fontWeight: '700', color: t.text,
|
|
229
|
+
textAlign: 'center', letterSpacing: -0.3,
|
|
230
|
+
},
|
|
231
|
+
subtitle: {
|
|
232
|
+
fontSize: 13, color: t.subtext, lineHeight: 19, textAlign: 'center',
|
|
233
|
+
},
|
|
234
|
+
taskDots: { flexDirection: 'row', gap: 6 },
|
|
235
|
+
dot: {
|
|
236
|
+
flex: 1, height: 2.5, borderRadius: 999, backgroundColor: t.border,
|
|
237
|
+
},
|
|
238
|
+
dotDone: { backgroundColor: t.accent, opacity: 0.35 },
|
|
239
|
+
dotActive: { backgroundColor: t.accent },
|
|
128
240
|
viewport: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
borderRadius: t.radius,
|
|
132
|
-
overflow: 'hidden',
|
|
133
|
-
backgroundColor: '#000',
|
|
134
|
-
marginBottom: 16,
|
|
135
|
-
alignSelf: 'center',
|
|
136
|
-
maxWidth: 280,
|
|
137
|
-
position: 'relative',
|
|
241
|
+
flex: 1, borderRadius: t.radius - 4, overflow: 'hidden',
|
|
242
|
+
backgroundColor: '#000', position: 'relative',
|
|
138
243
|
},
|
|
139
244
|
ovalGuide: {
|
|
140
|
-
position: 'absolute',
|
|
141
|
-
|
|
142
|
-
left: '12%',
|
|
143
|
-
right: '12%',
|
|
144
|
-
bottom: '8%',
|
|
145
|
-
borderWidth: 2,
|
|
146
|
-
borderColor: 'rgba(255,255,255,0.7)',
|
|
147
|
-
borderRadius: 999,
|
|
245
|
+
position: 'absolute', left: '21%', right: '21%', top: '6%', bottom: '6%',
|
|
246
|
+
borderWidth: 2, borderColor: 'rgba(255,255,255,0.75)', borderRadius: 999,
|
|
148
247
|
},
|
|
149
248
|
recBadge: {
|
|
150
|
-
position: 'absolute',
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
backgroundColor: 'rgba(220,38,38,0.85)',
|
|
156
|
-
borderRadius: 999,
|
|
157
|
-
paddingHorizontal: 10,
|
|
158
|
-
paddingVertical: 4,
|
|
159
|
-
gap: 6,
|
|
249
|
+
position: 'absolute', top: 10, left: 10,
|
|
250
|
+
flexDirection: 'row', alignItems: 'center', gap: 5,
|
|
251
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
252
|
+
borderWidth: 1, borderColor: 'rgba(239,68,68,0.5)',
|
|
253
|
+
borderRadius: 5, paddingHorizontal: 8, paddingVertical: 4,
|
|
160
254
|
},
|
|
161
255
|
recDot: {
|
|
162
|
-
width:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
256
|
+
width: 7, height: 7, borderRadius: 3.5, backgroundColor: '#ef4444',
|
|
257
|
+
},
|
|
258
|
+
recText: { color: '#ef4444', fontSize: 11, fontWeight: '700', letterSpacing: 0.5 },
|
|
259
|
+
overlay: {
|
|
260
|
+
position: 'absolute', bottom: 0, left: 0, right: 0,
|
|
261
|
+
paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12,
|
|
262
|
+
gap: 8,
|
|
263
|
+
backgroundColor: 'transparent',
|
|
264
|
+
// Gradient-like: darker at bottom via background trick
|
|
265
|
+
},
|
|
266
|
+
overlayPill: {
|
|
267
|
+
flexDirection: 'row', alignItems: 'center', gap: 8,
|
|
268
|
+
},
|
|
269
|
+
overlayNum: {
|
|
270
|
+
fontSize: 11, fontWeight: '700', color: 'rgba(255,255,255,0.55)',
|
|
271
|
+
textTransform: 'uppercase', letterSpacing: 0.5,
|
|
272
|
+
},
|
|
273
|
+
overlayLabel: {
|
|
274
|
+
fontSize: 15, fontWeight: '700', color: '#fff',
|
|
166
275
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
bottom: 12,
|
|
171
|
-
alignSelf: 'center',
|
|
172
|
-
width: 44,
|
|
173
|
-
height: 44,
|
|
174
|
-
borderRadius: 22,
|
|
175
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
176
|
-
alignItems: 'center',
|
|
177
|
-
justifyContent: 'center',
|
|
276
|
+
barWrap: {
|
|
277
|
+
height: 3, backgroundColor: 'rgba(255,255,255,0.2)',
|
|
278
|
+
borderRadius: 999, overflow: 'hidden',
|
|
178
279
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
backgroundColor: t.accent,
|
|
182
|
-
borderRadius: t.radius,
|
|
183
|
-
paddingVertical: 14,
|
|
184
|
-
alignItems: 'center',
|
|
185
|
-
marginBottom: 10,
|
|
280
|
+
barFill: {
|
|
281
|
+
height: '100%', backgroundColor: '#fff', borderRadius: 999,
|
|
186
282
|
},
|
|
187
|
-
|
|
188
|
-
backgroundColor: t.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
alignItems: 'center',
|
|
192
|
-
borderWidth: 1,
|
|
193
|
-
borderColor: t.border,
|
|
283
|
+
btn: {
|
|
284
|
+
backgroundColor: t.accent, borderRadius: t.radius - 4,
|
|
285
|
+
paddingVertical: 12, flexDirection: 'row',
|
|
286
|
+
alignItems: 'center', justifyContent: 'center',
|
|
194
287
|
},
|
|
195
|
-
btnText: { color:
|
|
196
|
-
|
|
288
|
+
btnText: { color: t.accentInv, fontSize: 14, fontWeight: '600' },
|
|
289
|
+
btnArrow: { color: t.accentInv, fontSize: 14, fontWeight: '600' },
|
|
197
290
|
})
|
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { View, Text,
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native'
|
|
3
3
|
import type { ResolvedTheme } from '../theme'
|
|
4
4
|
import type { VerifyLocale } from '../types'
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
|
-
theme:
|
|
7
|
+
theme: ResolvedTheme
|
|
8
8
|
locale: VerifyLocale
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function ProcessingStep({ theme, locale }: Props) {
|
|
12
|
-
const spinAnim = useRef(new Animated.Value(0)).current
|
|
13
12
|
const s = styles(theme)
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
Animated.loop(
|
|
17
|
-
Animated.timing(spinAnim, {
|
|
18
|
-
toValue: 1,
|
|
19
|
-
duration: 800,
|
|
20
|
-
useNativeDriver: true,
|
|
21
|
-
})
|
|
22
|
-
).start()
|
|
23
|
-
}, [spinAnim])
|
|
24
|
-
|
|
25
|
-
const spin = spinAnim.interpolate({
|
|
26
|
-
inputRange: [0, 1],
|
|
27
|
-
outputRange: ['0deg', '360deg'],
|
|
28
|
-
})
|
|
29
|
-
|
|
30
13
|
return (
|
|
31
14
|
<View style={s.container}>
|
|
32
|
-
<
|
|
15
|
+
<ActivityIndicator color={theme.accent} size="large" />
|
|
33
16
|
<Text style={s.title}>{locale.processing_title}</Text>
|
|
34
|
-
<Text style={s.subtitle}>
|
|
17
|
+
<Text style={s.subtitle}>This usually takes under 10 seconds.</Text>
|
|
35
18
|
</View>
|
|
36
19
|
)
|
|
37
20
|
}
|
|
@@ -39,27 +22,20 @@ export function ProcessingStep({ theme, locale }: Props) {
|
|
|
39
22
|
const styles = (t: ResolvedTheme) =>
|
|
40
23
|
StyleSheet.create({
|
|
41
24
|
container: {
|
|
42
|
-
|
|
25
|
+
flex: 1,
|
|
43
26
|
alignItems: 'center',
|
|
27
|
+
justifyContent: 'center',
|
|
44
28
|
gap: 16,
|
|
45
|
-
|
|
46
|
-
spinner: {
|
|
47
|
-
width: 52,
|
|
48
|
-
height: 52,
|
|
49
|
-
borderRadius: 26,
|
|
50
|
-
borderWidth: 4,
|
|
51
|
-
borderColor: t.border,
|
|
52
|
-
borderTopColor: t.accent,
|
|
53
|
-
marginBottom: 8,
|
|
29
|
+
padding: 32,
|
|
54
30
|
},
|
|
55
31
|
title: {
|
|
56
|
-
fontSize:
|
|
32
|
+
fontSize: 16,
|
|
57
33
|
fontWeight: '700',
|
|
58
34
|
color: t.text,
|
|
59
35
|
textAlign: 'center',
|
|
60
36
|
},
|
|
61
37
|
subtitle: {
|
|
62
|
-
fontSize:
|
|
38
|
+
fontSize: 13,
|
|
63
39
|
color: t.subtext,
|
|
64
40
|
textAlign: 'center',
|
|
65
41
|
},
|