@uverifyng/react-native-liveness 0.1.0
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 +70 -0
- package/dist/UVerifyWebView.d.ts +26 -0
- package/dist/UVerifyWebView.js +116 -0
- package/dist/events.d.ts +24 -0
- package/dist/events.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# UVerify liveness for React Native
|
|
2
|
+
|
|
3
|
+
UVerify’s face check (liveness), and verification links, inside your React Native app.
|
|
4
|
+
|
|
5
|
+
The check runs on UVerify’s hosted page, the same one your customers get in a browser, so every anti-spoofing layer (depth, colour flash, random prompts) applies, and improvements reach your app without an app update.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @uverifyng/react-native-liveness react-native-webview
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
React Native 0.72+, `react-native-webview` 13+. iOS 14.3+ (camera inside web views), Android 7+.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
**iOS** — `Info.plist`:
|
|
16
|
+
|
|
17
|
+
```xml
|
|
18
|
+
<key>NSCameraUsageDescription</key>
|
|
19
|
+
<string>We use the camera to confirm it’s really you.</string>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Android** — `AndroidManifest.xml`:
|
|
23
|
+
|
|
24
|
+
```xml
|
|
25
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Use
|
|
29
|
+
|
|
30
|
+
Your **server** creates the session with a UVerify server SDK and returns its `url` and `id`. Never put your API key in the app.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { UVerifyWebView } from '@uverifyng/react-native-liveness';
|
|
34
|
+
|
|
35
|
+
function FaceCheck({ session, onDone }) {
|
|
36
|
+
return (
|
|
37
|
+
<UVerifyWebView
|
|
38
|
+
url={session.url}
|
|
39
|
+
onFinish={(result) => {
|
|
40
|
+
if (result.type === 'liveness') onDone(result.sessionId, result.status); // 'passed' | 'failed' | 'expired'
|
|
41
|
+
}}
|
|
42
|
+
onCancel={(reason) => onDone(null, reason)} // 'back' | 'camera_denied'
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then, on your server, run the face match with that session:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await uverify.identity.bvnFaceMatch({ id_number, first_name, last_name, liveness_session_id: sessionId });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The result in the app is only a hint to move on; **trust what your server gets from the API**.
|
|
55
|
+
|
|
56
|
+
### Verification links
|
|
57
|
+
|
|
58
|
+
The same component opens a verification link (`link.url` from `kyc.createLink`): `onFinish` receives `{ type: 'kyc', id, status, outcome }`.
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
| Prop | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `url` | The session or link `url` from your server |
|
|
65
|
+
| `redirectUrl` | The `redirect_url` you created it with, if any; reaching it also finishes (`{ type: 'redirect', params }`) |
|
|
66
|
+
| `onFinish(result)` | The flow finished |
|
|
67
|
+
| `onCancel(reason)` | Back button, or the camera was refused |
|
|
68
|
+
| `allowedHosts` | Extra hosts allowed in the view (UVerify’s are always allowed; everything else is blocked) |
|
|
69
|
+
|
|
70
|
+
MIT licence.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { type UVerifyResult } from './events';
|
|
4
|
+
export interface UVerifyWebViewProps {
|
|
5
|
+
/** A liveness session's `url`, or a verification link's `url` (from your server, via the UVerify API). */
|
|
6
|
+
url: string;
|
|
7
|
+
/** The redirect_url you created the session/link with, if any: reaching it also finishes. */
|
|
8
|
+
redirectUrl?: string;
|
|
9
|
+
/** The flow finished. For liveness, pass `result.sessionId` to your server for the face match. */
|
|
10
|
+
onFinish: (result: UVerifyResult) => void;
|
|
11
|
+
/** The person backed out (Android back button) or refused the camera. */
|
|
12
|
+
onCancel?: (reason: 'back' | 'camera_denied') => void;
|
|
13
|
+
/** Extra hosts to allow (default: uverify.com.ng and its subdomains). */
|
|
14
|
+
allowedHosts?: string[];
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* UVerify's hosted face check (or a verification link) inside your app. The
|
|
19
|
+
* check itself runs on UVerify's page, the same one customers get in a
|
|
20
|
+
* browser, so every anti-spoofing layer applies and improvements reach your
|
|
21
|
+
* app without an update.
|
|
22
|
+
*
|
|
23
|
+
* Needs the camera permission: add NSCameraUsageDescription to Info.plist and
|
|
24
|
+
* <uses-permission android:name="android.permission.CAMERA" /> to AndroidManifest.xml.
|
|
25
|
+
*/
|
|
26
|
+
export declare function UVerifyWebView({ url, redirectUrl, onFinish, onCancel, allowedHosts, style }: UVerifyWebViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UVerifyWebView = UVerifyWebView;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const react_native_webview_1 = require("react-native-webview");
|
|
40
|
+
const events_1 = require("./events");
|
|
41
|
+
/**
|
|
42
|
+
* UVerify's hosted face check (or a verification link) inside your app. The
|
|
43
|
+
* check itself runs on UVerify's page, the same one customers get in a
|
|
44
|
+
* browser, so every anti-spoofing layer applies and improvements reach your
|
|
45
|
+
* app without an update.
|
|
46
|
+
*
|
|
47
|
+
* Needs the camera permission: add NSCameraUsageDescription to Info.plist and
|
|
48
|
+
* <uses-permission android:name="android.permission.CAMERA" /> to AndroidManifest.xml.
|
|
49
|
+
*/
|
|
50
|
+
function UVerifyWebView({ url, redirectUrl, onFinish, onCancel, allowedHosts = [], style }) {
|
|
51
|
+
const [cameraOk, setCameraOk] = (0, react_1.useState)(react_native_1.Platform.OS !== 'android');
|
|
52
|
+
const [denied, setDenied] = (0, react_1.useState)(false);
|
|
53
|
+
const done = (0, react_1.useRef)(false);
|
|
54
|
+
const hosts = ['uverify.com.ng', ...allowedHosts.map((h) => h.toLowerCase())];
|
|
55
|
+
const finish = (0, react_1.useCallback)((r) => {
|
|
56
|
+
if (done.current)
|
|
57
|
+
return;
|
|
58
|
+
done.current = true;
|
|
59
|
+
onFinish(r);
|
|
60
|
+
}, [onFinish]);
|
|
61
|
+
// Android: ask for the camera before the page does (the web view can only use it if the app has it).
|
|
62
|
+
(0, react_1.useEffect)(() => {
|
|
63
|
+
if (react_native_1.Platform.OS !== 'android')
|
|
64
|
+
return;
|
|
65
|
+
react_native_1.PermissionsAndroid.request(react_native_1.PermissionsAndroid.PERMISSIONS.CAMERA).then((r) => {
|
|
66
|
+
if (r === react_native_1.PermissionsAndroid.RESULTS.GRANTED)
|
|
67
|
+
setCameraOk(true);
|
|
68
|
+
else {
|
|
69
|
+
setDenied(true);
|
|
70
|
+
onCancel?.('camera_denied');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}, [onCancel]);
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
const sub = react_native_1.BackHandler.addEventListener('hardwareBackPress', () => {
|
|
76
|
+
if (done.current)
|
|
77
|
+
return false;
|
|
78
|
+
done.current = true;
|
|
79
|
+
onCancel?.('back');
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
return () => sub.remove();
|
|
83
|
+
}, [onCancel]);
|
|
84
|
+
const onMessage = (e) => {
|
|
85
|
+
const r = (0, events_1.parseMessage)(e.nativeEvent.data);
|
|
86
|
+
if (r)
|
|
87
|
+
finish(r);
|
|
88
|
+
};
|
|
89
|
+
const onShouldStart = (req) => {
|
|
90
|
+
const r = (0, events_1.parseRedirect)(req.url, redirectUrl);
|
|
91
|
+
if (r) {
|
|
92
|
+
finish(r);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return (0, events_1.isAllowed)(req.url, hosts);
|
|
96
|
+
};
|
|
97
|
+
if (denied) {
|
|
98
|
+
return (react_1.default.createElement(react_native_1.View, { style: [styles.center, style] },
|
|
99
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text }, "Camera access is needed for the face check. Allow it in Settings, then try again.")));
|
|
100
|
+
}
|
|
101
|
+
if (!cameraOk) {
|
|
102
|
+
return (react_1.default.createElement(react_native_1.View, { style: [styles.center, style] },
|
|
103
|
+
react_1.default.createElement(react_native_1.ActivityIndicator, null)));
|
|
104
|
+
}
|
|
105
|
+
return (react_1.default.createElement(react_native_webview_1.WebView, { style: [styles.fill, style], source: { uri: url }, originWhitelist: ['https://*'], onMessage: onMessage, onShouldStartLoadWithRequest: onShouldStart, javaScriptEnabled: true, domStorageEnabled: true,
|
|
106
|
+
// iOS: camera inside the page, inline (not full-screen), no second prompt from WebKit.
|
|
107
|
+
allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, mediaCapturePermissionGrantType: "grant",
|
|
108
|
+
// Android: react-native-webview passes the page's camera request through once the app holds CAMERA (asked above).
|
|
109
|
+
setSupportMultipleWindows: false, startInLoadingState: true, renderLoading: () => (react_1.default.createElement(react_native_1.View, { style: styles.center },
|
|
110
|
+
react_1.default.createElement(react_native_1.ActivityIndicator, null))) }));
|
|
111
|
+
}
|
|
112
|
+
const styles = react_native_1.StyleSheet.create({
|
|
113
|
+
fill: { flex: 1 },
|
|
114
|
+
center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
|
|
115
|
+
text: { textAlign: 'center', fontSize: 15, color: '#444' },
|
|
116
|
+
});
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic, shared by the component and its tests: what the hosted page
|
|
3
|
+
* tells the app, and how a redirect back to your site is read.
|
|
4
|
+
*/
|
|
5
|
+
export type UVerifyResult = {
|
|
6
|
+
type: 'liveness';
|
|
7
|
+
sessionId: string;
|
|
8
|
+
status: 'passed' | 'failed' | 'expired';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'kyc';
|
|
11
|
+
id: string;
|
|
12
|
+
status: string;
|
|
13
|
+
outcome: string | null;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'redirect';
|
|
16
|
+
url: string;
|
|
17
|
+
params: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
/** The hosted page's "finished" message (posted by uverify.com.ng), or null if it's something else. */
|
|
20
|
+
export declare function parseMessage(data: string): UVerifyResult | null;
|
|
21
|
+
/** The page is sending the person back to your redirect_url: finish with its query parameters. */
|
|
22
|
+
export declare function parseRedirect(url: string, redirectUrl: string | undefined): UVerifyResult | null;
|
|
23
|
+
/** Only UVerify's pages (and about:blank) load in the view; anything else is refused. */
|
|
24
|
+
export declare function isAllowed(url: string, hosts: string[]): boolean;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMessage = parseMessage;
|
|
4
|
+
exports.parseRedirect = parseRedirect;
|
|
5
|
+
exports.isAllowed = isAllowed;
|
|
6
|
+
/** The hosted page's "finished" message (posted by uverify.com.ng), or null if it's something else. */
|
|
7
|
+
function parseMessage(data) {
|
|
8
|
+
let m;
|
|
9
|
+
try {
|
|
10
|
+
m = JSON.parse(data);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (m?.source !== 'uverify')
|
|
16
|
+
return null;
|
|
17
|
+
if (m.type === 'liveness.finished' && typeof m.session_id === 'string')
|
|
18
|
+
return { type: 'liveness', sessionId: m.session_id, status: m.status };
|
|
19
|
+
if (m.type === 'kyc.finished' && typeof m.id === 'string')
|
|
20
|
+
return { type: 'kyc', id: m.id, status: m.status, outcome: m.outcome ?? null };
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/** The page is sending the person back to your redirect_url: finish with its query parameters. */
|
|
24
|
+
function parseRedirect(url, redirectUrl) {
|
|
25
|
+
if (!redirectUrl)
|
|
26
|
+
return null;
|
|
27
|
+
const strip = (u) => u.split('?')[0].split('#')[0].replace(/\/+$/, '');
|
|
28
|
+
if (strip(url) !== strip(redirectUrl))
|
|
29
|
+
return null;
|
|
30
|
+
const params = {};
|
|
31
|
+
const q = url.split('#')[0].split('?')[1] ?? '';
|
|
32
|
+
for (const pair of q.split('&').filter(Boolean)) {
|
|
33
|
+
const [k, v = ''] = pair.split('=');
|
|
34
|
+
params[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' '));
|
|
35
|
+
}
|
|
36
|
+
return { type: 'redirect', url, params };
|
|
37
|
+
}
|
|
38
|
+
/** Only UVerify's pages (and about:blank) load in the view; anything else is refused. */
|
|
39
|
+
function isAllowed(url, hosts) {
|
|
40
|
+
if (url === 'about:blank')
|
|
41
|
+
return true;
|
|
42
|
+
const m = url.match(/^https:\/\/([^/?#:]+)/i);
|
|
43
|
+
return !!m && hosts.some((h) => m[1].toLowerCase() === h || m[1].toLowerCase().endsWith(`.${h}`));
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRedirect = exports.parseMessage = exports.isAllowed = exports.UVerifyWebView = void 0;
|
|
4
|
+
var UVerifyWebView_1 = require("./UVerifyWebView");
|
|
5
|
+
Object.defineProperty(exports, "UVerifyWebView", { enumerable: true, get: function () { return UVerifyWebView_1.UVerifyWebView; } });
|
|
6
|
+
var events_1 = require("./events");
|
|
7
|
+
Object.defineProperty(exports, "isAllowed", { enumerable: true, get: function () { return events_1.isAllowed; } });
|
|
8
|
+
Object.defineProperty(exports, "parseMessage", { enumerable: true, get: function () { return events_1.parseMessage; } });
|
|
9
|
+
Object.defineProperty(exports, "parseRedirect", { enumerable: true, get: function () { return events_1.parseRedirect; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uverifyng/react-native-liveness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UVerify's face check (liveness) and verification links inside a React Native app.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Elasto Web Services Limited",
|
|
7
|
+
"homepage": "https://uverify.com.ng/docs",
|
|
8
|
+
"repository": { "type": "git", "url": "https://github.com/elastodev/uverify-sdks", "directory": "react-native" },
|
|
9
|
+
"keywords": ["uverify", "liveness", "face-match", "kyc", "react-native", "nigeria"],
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"files": ["dist", "README.md"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"test": "vitest run"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=18",
|
|
20
|
+
"react-native": ">=0.72",
|
|
21
|
+
"react-native-webview": ">=13"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^18.3.0",
|
|
25
|
+
"react": "^18.3.1",
|
|
26
|
+
"react-native": "^0.76.0",
|
|
27
|
+
"react-native-webview": "^13.12.0",
|
|
28
|
+
"typescript": "^5.6.0",
|
|
29
|
+
"vitest": "^2.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|