@valifysolutions/react-native-vidvdigitalcontract 1.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 +240 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/vidv/digitalcontract/VIDVDigitalContractModule.kt +111 -0
- package/android/src/main/kotlin/com/vidv/digitalcontract/VIDVDigitalContractPackage.kt +14 -0
- package/ios/VIDVDigitalContractModule.m +16 -0
- package/ios/VIDVDigitalContractModule.swift +156 -0
- package/package.json +48 -0
- package/react-native.config.js +11 -0
- package/src/index.ts +60 -0
- package/src/types.ts +40 -0
- package/vidvdigitalcontract-react-native.podspec +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# VIDV Digital Contract — React Native Plugin
|
|
2
|
+
|
|
3
|
+
A React Native bridge for the **VIDV Digital Contract** native SDK. Identical functionality,
|
|
4
|
+
API design, and flow to the Flutter plugin (`VIDVDigitalContractFlutter`).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
| Platform | Minimum version |
|
|
11
|
+
|----------|----------------|
|
|
12
|
+
| iOS | 13.0 |
|
|
13
|
+
| Android | API 24 (7.0) |
|
|
14
|
+
| React Native | ≥ 0.70 |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# npm
|
|
22
|
+
npm install vidv-digital-contract-react-native
|
|
23
|
+
|
|
24
|
+
# yarn
|
|
25
|
+
yarn add vidv-digital-contract-react-native
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Android — repository credentials
|
|
29
|
+
|
|
30
|
+
The native SDK is hosted on Valify's private Artifactory. Add the repository to your
|
|
31
|
+
**project-level** `build.gradle` (or `settings.gradle` for newer React Native templates):
|
|
32
|
+
|
|
33
|
+
```groovy
|
|
34
|
+
repositories {
|
|
35
|
+
maven {
|
|
36
|
+
credentials {
|
|
37
|
+
username "sdk"
|
|
38
|
+
password "sdk123456"
|
|
39
|
+
}
|
|
40
|
+
url "https://www.valifystage.com/artifactory/libs-release/"
|
|
41
|
+
}
|
|
42
|
+
maven { url "https://jitpack.io" }
|
|
43
|
+
maven { url "https://developer.huawei.com/repo/" }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Android — manifest entries
|
|
48
|
+
|
|
49
|
+
Add the following to your app's `AndroidManifest.xml`. Both the main activity and the
|
|
50
|
+
SDK host activity **must share the same `taskAffinity`** so the flow returns correctly:
|
|
51
|
+
|
|
52
|
+
```xml
|
|
53
|
+
<activity
|
|
54
|
+
android:name=".MainActivity"
|
|
55
|
+
android:taskAffinity="com.yourapp.example"
|
|
56
|
+
android:launchMode="singleTop"
|
|
57
|
+
... />
|
|
58
|
+
|
|
59
|
+
<!-- Required SDK host activity -->
|
|
60
|
+
<activity
|
|
61
|
+
android:name="me.vidv.vidvdigitalcontractsdk.VIDVDigitalContractHostActivity"
|
|
62
|
+
android:exported="true"
|
|
63
|
+
android:taskAffinity="com.yourapp.example"
|
|
64
|
+
android:documentLaunchMode="never"
|
|
65
|
+
android:excludeFromRecents="true"
|
|
66
|
+
tools:replace="android:taskAffinity,android:documentLaunchMode,android:excludeFromRecents" />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Also add packaging exclusions in `app/build.gradle`:
|
|
70
|
+
|
|
71
|
+
```groovy
|
|
72
|
+
packagingOptions {
|
|
73
|
+
pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### iOS — CocoaPods
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
cd ios && pod install
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The plugin's podspec depends on `VIDVDigitalContract ~> 1.1.0`. Make sure your team has
|
|
84
|
+
access to the CocoaPods source for this pod, or point your Podfile to the private spec repo.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { startDigitalContract } from 'vidv-digital-contract-react-native';
|
|
92
|
+
import type { DigitalContractResponse } from 'vidv-digital-contract-react-native';
|
|
93
|
+
|
|
94
|
+
async function launch() {
|
|
95
|
+
const response: DigitalContractResponse = await startDigitalContract({
|
|
96
|
+
vidvBaseUrl: 'https://www.valifystage.com/',
|
|
97
|
+
dcBaseUrl: 'https://valify.staging.knfrm.com/',
|
|
98
|
+
bundleKey: '<your-bundle-key>',
|
|
99
|
+
vidvAccessToken: '<vidv-oauth-token>',
|
|
100
|
+
dcAccessToken: '<dc-auth-token>',
|
|
101
|
+
tenantId: '<tenant-id>',
|
|
102
|
+
userReferenceId: 'user-123',
|
|
103
|
+
language: 'en', // 'en' or 'ar'
|
|
104
|
+
templateVersionId: 2,
|
|
105
|
+
contractExpiryMinutes: 30,
|
|
106
|
+
contractData: JSON.stringify([
|
|
107
|
+
{ field_id: '1', value: 'John' },
|
|
108
|
+
{ field_id: '2', value: 'Doe' },
|
|
109
|
+
]),
|
|
110
|
+
allowHandSignature: false, // optional
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
switch (response.state) {
|
|
114
|
+
case 'SUCCESS':
|
|
115
|
+
console.log('Contract signed!', response.result);
|
|
116
|
+
break;
|
|
117
|
+
case 'FAILURE':
|
|
118
|
+
console.warn('Service failure', response.result);
|
|
119
|
+
break;
|
|
120
|
+
case 'ERROR':
|
|
121
|
+
console.error('Builder error', response.result);
|
|
122
|
+
break;
|
|
123
|
+
case 'EXIT':
|
|
124
|
+
console.log('User exited', response.result);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## API
|
|
133
|
+
|
|
134
|
+
### `startDigitalContract(config)`
|
|
135
|
+
|
|
136
|
+
Returns `Promise<DigitalContractResponse>`.
|
|
137
|
+
|
|
138
|
+
#### `DigitalContractConfig`
|
|
139
|
+
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
|-------|------|----------|-------------|
|
|
142
|
+
| `vidvBaseUrl` | `string` | ✓ | VIDV backend base URL |
|
|
143
|
+
| `dcBaseUrl` | `string` | ✓ | Digital Contract backend base URL |
|
|
144
|
+
| `bundleKey` | `string` | ✓ | Your integration bundle key |
|
|
145
|
+
| `vidvAccessToken` | `string` | ✓ | OAuth token from VIDV (`/api/o/token/`) |
|
|
146
|
+
| `dcAccessToken` | `string` | ✓ | Auth token from DC (`/api/v1/auth-token`) |
|
|
147
|
+
| `tenantId` | `string` | ✓ | Tenant ID returned with DC token |
|
|
148
|
+
| `userReferenceId` | `string` | ✓ | Your internal user identifier |
|
|
149
|
+
| `language` | `'en' \| 'ar'` | ✓ | UI language |
|
|
150
|
+
| `templateVersionId` | `number` | ✓ | Contract template version (> 0) |
|
|
151
|
+
| `contractExpiryMinutes` | `number` | ✓ | Session expiry in minutes (> 0) |
|
|
152
|
+
| `contractData` | `string` | ✓ | JSON array of `{field_id, value}` objects |
|
|
153
|
+
| `allowHandSignature` | `boolean` | — | Enable hand-signature capture |
|
|
154
|
+
|
|
155
|
+
#### `DigitalContractResponse`
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
{
|
|
159
|
+
state: 'SUCCESS' | 'ERROR' | 'FAILURE' | 'EXIT';
|
|
160
|
+
result: {
|
|
161
|
+
// SUCCESS
|
|
162
|
+
data?: { contractID?: string; iframeURL?: string; contractStatus?: string };
|
|
163
|
+
|
|
164
|
+
// ERROR (builder validation)
|
|
165
|
+
code?: string | number;
|
|
166
|
+
message?: string;
|
|
167
|
+
|
|
168
|
+
// FAILURE (service error)
|
|
169
|
+
code?: string | number;
|
|
170
|
+
message?: string;
|
|
171
|
+
data?: DigitalContractData;
|
|
172
|
+
sdkError?: { errorCode?: string | number; message?: string };
|
|
173
|
+
|
|
174
|
+
// EXIT (user cancelled)
|
|
175
|
+
step?: string;
|
|
176
|
+
data?: DigitalContractData;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Token fetching
|
|
184
|
+
|
|
185
|
+
Tokens are **not** fetched by the plugin — your app must obtain them before calling
|
|
186
|
+
`startDigitalContract`.
|
|
187
|
+
|
|
188
|
+
### VIDV token (OAuth password grant)
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const res = await fetch(`${vidvBaseUrl}/api/o/token/`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
194
|
+
body: `username=...&password=...&client_id=...&client_secret=...&grant_type=password`,
|
|
195
|
+
});
|
|
196
|
+
const { access_token } = await res.json();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### DC token (HMAC-SHA256 authenticated)
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import CryptoJS from 'crypto-js';
|
|
203
|
+
|
|
204
|
+
const body = JSON.stringify({ username, password, client_id, client_secret });
|
|
205
|
+
const hmac = CryptoJS.HmacSHA256(body, secretKey).toString(CryptoJS.enc.Hex);
|
|
206
|
+
|
|
207
|
+
const res = await fetch(`${dcBaseUrl}/api/v1/auth-token`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json', hmac },
|
|
210
|
+
body,
|
|
211
|
+
});
|
|
212
|
+
const { data: { token, tenant_id } } = await res.json();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Example app
|
|
218
|
+
|
|
219
|
+
A complete demo app is located in `example/`. It mirrors the Flutter example app exactly.
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
cd example
|
|
223
|
+
yarn install
|
|
224
|
+
|
|
225
|
+
# iOS
|
|
226
|
+
cd ios && pod install && cd ..
|
|
227
|
+
yarn ios
|
|
228
|
+
|
|
229
|
+
# Android
|
|
230
|
+
yarn android
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Native SDK versions
|
|
236
|
+
|
|
237
|
+
| Platform | SDK | Version |
|
|
238
|
+
|----------|-----|---------|
|
|
239
|
+
| Android | `com.vidv:vidvdigitalcontractsdk` | `1.2.1` |
|
|
240
|
+
| iOS | `VIDVDigitalContract` (CocoaPod) | `~> 1.1.0` |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
dependencies {
|
|
7
|
+
classpath "com.android.tools.build:gradle:8.1.4"
|
|
8
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
apply plugin: 'com.android.library'
|
|
13
|
+
apply plugin: 'kotlin-android'
|
|
14
|
+
|
|
15
|
+
android {
|
|
16
|
+
compileSdkVersion 35
|
|
17
|
+
namespace "com.vidv.digitalcontract"
|
|
18
|
+
|
|
19
|
+
compileOptions {
|
|
20
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
21
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
kotlinOptions {
|
|
25
|
+
jvmTarget = "17"
|
|
26
|
+
freeCompilerArgs += ["-Xskip-metadata-version-check"]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sourceSets {
|
|
30
|
+
main {
|
|
31
|
+
java.srcDirs += 'src/main/kotlin'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
defaultConfig {
|
|
36
|
+
minSdkVersion 24
|
|
37
|
+
targetSdkVersion 34
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
repositories {
|
|
42
|
+
google()
|
|
43
|
+
mavenCentral()
|
|
44
|
+
maven {
|
|
45
|
+
credentials {
|
|
46
|
+
username "sdk"
|
|
47
|
+
password "sdk123456"
|
|
48
|
+
}
|
|
49
|
+
url "https://www.valifystage.com/artifactory/libs-release/"
|
|
50
|
+
}
|
|
51
|
+
maven { url "https://jitpack.io" }
|
|
52
|
+
maven { url "https://developer.huawei.com/repo/" }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dependencies {
|
|
56
|
+
implementation "com.facebook.react:react-native:+"
|
|
57
|
+
implementation "com.vidv:vidvdigitalcontractsdk:1.2.3"
|
|
58
|
+
implementation "com.google.code.gson:gson:2.11.0"
|
|
59
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.vidv.digitalcontract
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.google.gson.Gson
|
|
10
|
+
import me.vidv.vidvdigitalcontractsdk.sdk.VIDVDigitalContractConfig
|
|
11
|
+
import me.vidv.vidvdigitalcontractsdk.sdk.VIDVDigitalContractData
|
|
12
|
+
import me.vidv.vidvdigitalcontractsdk.sdk.VIDVDigitalContractListener
|
|
13
|
+
import me.vidv.vidvdigitalcontractsdk.sdk.VIDVDigitalContractResponse
|
|
14
|
+
|
|
15
|
+
class VIDVDigitalContractModule(reactContext: ReactApplicationContext) :
|
|
16
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
17
|
+
|
|
18
|
+
private val gson = Gson()
|
|
19
|
+
private var pendingPromise: Promise? = null
|
|
20
|
+
|
|
21
|
+
override fun getName(): String = "VIDVDigitalContract"
|
|
22
|
+
|
|
23
|
+
@ReactMethod
|
|
24
|
+
fun startDigitalContract(args: ReadableMap, promise: Promise) {
|
|
25
|
+
if (pendingPromise != null) {
|
|
26
|
+
promise.reject("ALREADY_RUNNING", "Another Digital Contract flow is in progress.")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
val activity: Activity? = currentActivity
|
|
30
|
+
if (activity == null) {
|
|
31
|
+
promise.reject("NO_ACTIVITY", "Plugin is not attached to an Activity.")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
val argsMap: Map<String, Any?> = args.toHashMap()
|
|
35
|
+
pendingPromise = promise
|
|
36
|
+
try {
|
|
37
|
+
startSDK(activity, argsMap)
|
|
38
|
+
} catch (t: Throwable) {
|
|
39
|
+
pendingPromise?.reject("START_ERROR", t.message)
|
|
40
|
+
pendingPromise = null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun numberFromArg(value: Any?): Int? = when (value) {
|
|
45
|
+
is Int -> value
|
|
46
|
+
is Long -> value.toInt()
|
|
47
|
+
is Double -> value.toInt()
|
|
48
|
+
is String -> value.toIntOrNull()
|
|
49
|
+
else -> null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun startSDK(activity: Activity, args: Map<String, Any?>) {
|
|
53
|
+
val builder = VIDVDigitalContractConfig.Builder()
|
|
54
|
+
.setVIDVBaseUrl(args["vidv_base_url"] as String)
|
|
55
|
+
.setDCBaseUrl(args["dc_base_url"] as String)
|
|
56
|
+
.setVIDVAccessToken(args["vidv_access_token"] as String)
|
|
57
|
+
.setDCAccessToken(args["dc_access_token"] as String)
|
|
58
|
+
.setBundleKey(args["bundle_key"] as String)
|
|
59
|
+
.setTenantId(args["tenant_id"] as String)
|
|
60
|
+
.setUserReferenceId(args["user_reference_id"] as String)
|
|
61
|
+
.setLanguage(args["language"] as String)
|
|
62
|
+
.setTemplateVersionId(numberFromArg(args["template_version_id"]) ?: 0)
|
|
63
|
+
.setExpiryMinutes(numberFromArg(args["contract_expiry_minutes"]) ?: 0)
|
|
64
|
+
.setContractData(args["contract_data"] as String)
|
|
65
|
+
|
|
66
|
+
if ((args["allow_hand_signature"] as? Boolean) == true) {
|
|
67
|
+
builder.setAllowHandSignature(true)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
builder.start(activity, object : VIDVDigitalContractListener {
|
|
71
|
+
override fun onDigitalContractResult(result: VIDVDigitalContractResponse) {
|
|
72
|
+
when (result) {
|
|
73
|
+
is VIDVDigitalContractResponse.Success -> sendJson(
|
|
74
|
+
"SUCCESS",
|
|
75
|
+
mapOf("data" to serializeData(result.data))
|
|
76
|
+
)
|
|
77
|
+
is VIDVDigitalContractResponse.BuilderError -> sendJson(
|
|
78
|
+
"ERROR",
|
|
79
|
+
mapOf("code" to result.code, "message" to result.message)
|
|
80
|
+
)
|
|
81
|
+
is VIDVDigitalContractResponse.ServiceFailure -> sendJson(
|
|
82
|
+
"FAILURE",
|
|
83
|
+
mapOf(
|
|
84
|
+
"code" to result.code,
|
|
85
|
+
"message" to result.message,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
is VIDVDigitalContractResponse.Exit -> sendJson(
|
|
89
|
+
"EXIT",
|
|
90
|
+
mapOf("data" to serializeData(result.data))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun serializeData(data: VIDVDigitalContractData?): Map<String, Any?>? {
|
|
98
|
+
if (data == null) return null
|
|
99
|
+
return mapOf(
|
|
100
|
+
"contractID" to data.contractId,
|
|
101
|
+
"iframeURL" to data.iframeURL,
|
|
102
|
+
"contractStatus" to data.contractStatus
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun sendJson(state: String, result: Any) {
|
|
107
|
+
val payload = mapOf("state" to state, "result" to result)
|
|
108
|
+
pendingPromise?.resolve(gson.toJson(payload))
|
|
109
|
+
pendingPromise = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.vidv.digitalcontract
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class VIDVDigitalContractPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
listOf(VIDVDigitalContractModule(reactContext))
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
|
13
|
+
emptyList()
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_REMAP_MODULE(VIDVDigitalContract, VIDVDigitalContractModule, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(
|
|
6
|
+
startDigitalContract:(NSDictionary *)args
|
|
7
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
+ (BOOL)requiresMainQueueSetup
|
|
12
|
+
{
|
|
13
|
+
return YES;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import VIDVDigitalContract
|
|
4
|
+
|
|
5
|
+
// Swift-native type aliases that match the ObjC RCTPromise block signatures.
|
|
6
|
+
// Using these instead of RCTPromiseResolveBlock/RCTPromiseRejectBlock avoids
|
|
7
|
+
// a compile failure when React-Core is not imported as a Swift module
|
|
8
|
+
// (i.e. when use_frameworks! is NOT used, which is the common RN setup).
|
|
9
|
+
typealias Resolve = (Any?) -> Void
|
|
10
|
+
typealias Reject = (String, String, Error?) -> Void
|
|
11
|
+
|
|
12
|
+
@objc(VIDVDigitalContractModule)
|
|
13
|
+
class VIDVDigitalContractModule: NSObject {
|
|
14
|
+
|
|
15
|
+
private static var pendingResolve: Resolve?
|
|
16
|
+
private static var pendingReject: Reject?
|
|
17
|
+
private static var delegateHolder = DigitalContractDelegateHolder()
|
|
18
|
+
|
|
19
|
+
// The selector name must exactly match the RCT_EXTERN_METHOD declaration in .m
|
|
20
|
+
@objc(startDigitalContract:resolve:rejecter:)
|
|
21
|
+
func startDigitalContract(
|
|
22
|
+
_ args: NSDictionary,
|
|
23
|
+
resolve: @escaping Resolve,
|
|
24
|
+
rejecter reject: @escaping Reject
|
|
25
|
+
) {
|
|
26
|
+
guard Self.pendingResolve == nil else {
|
|
27
|
+
reject("ALREADY_RUNNING", "Another Digital Contract flow is in progress.", nil)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
guard let argsDict = args as? [String: Any] else {
|
|
32
|
+
reject("INVALID_ARGS", "Arguments must be a map.", nil)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Store callbacks before dispatching to main thread
|
|
37
|
+
Self.pendingResolve = resolve
|
|
38
|
+
Self.pendingReject = reject
|
|
39
|
+
|
|
40
|
+
Self.delegateHolder.onResult = { json in
|
|
41
|
+
Self.pendingResolve?(json)
|
|
42
|
+
Self.pendingResolve = nil
|
|
43
|
+
Self.pendingReject = nil
|
|
44
|
+
}
|
|
45
|
+
Self.delegateHolder.onError = { code, message in
|
|
46
|
+
Self.pendingReject?(code, message, nil)
|
|
47
|
+
Self.pendingResolve = nil
|
|
48
|
+
Self.pendingReject = nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// All UIKit access (topViewController + builder.start) must be on the main thread
|
|
52
|
+
DispatchQueue.main.async {
|
|
53
|
+
guard let rootVC = Self.topViewController() else {
|
|
54
|
+
Self.pendingReject?("NO_VIEW_CONTROLLER", "No view controller to present from.", nil)
|
|
55
|
+
Self.pendingResolve = nil
|
|
56
|
+
Self.pendingReject = nil
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
Self.startSDK(from: rootVC, args: argsDict, delegate: Self.delegateHolder)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static func intFrom(_ value: Any?) -> Int? {
|
|
64
|
+
switch value {
|
|
65
|
+
case let n as Int: return n
|
|
66
|
+
case let n as Int64: return Int(n)
|
|
67
|
+
case let n as Double: return Int(n)
|
|
68
|
+
case let s as String: return Int(s)
|
|
69
|
+
default: return nil
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static func startSDK(
|
|
74
|
+
from viewController: UIViewController,
|
|
75
|
+
args: [String: Any],
|
|
76
|
+
delegate: VIDVDigitalContractDelegate
|
|
77
|
+
) {
|
|
78
|
+
var builder = VIDVDigitalContractBuilder()
|
|
79
|
+
.setVIDVBaseUrl(args["vidv_base_url"] as! String)
|
|
80
|
+
.setDCBaseUrl(args["dc_base_url"] as! String)
|
|
81
|
+
.setVIDVAccessToken(args["vidv_access_token"] as! String)
|
|
82
|
+
.setDcAccessToken(args["dc_access_token"] as! String)
|
|
83
|
+
.setBundleKey(args["bundle_key"] as! String)
|
|
84
|
+
.setTenantId(args["tenant_id"] as! String)
|
|
85
|
+
.setUserReferenceId(args["user_reference_id"] as! String)
|
|
86
|
+
.setLanguage(args["language"] as! String)
|
|
87
|
+
.setTempleteVersionId(intFrom(args["template_version_id"]) ?? 0)
|
|
88
|
+
.setContractExpiryMinutes(intFrom(args["contract_expiry_minutes"]) ?? 0)
|
|
89
|
+
.contractData(args["contract_data"] as! String)
|
|
90
|
+
|
|
91
|
+
if args["allow_hand_signature"] as? Bool == true {
|
|
92
|
+
builder = builder.allowHandSignature(true)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
builder.start(viewController, delegate: delegate)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static func topViewController() -> UIViewController? {
|
|
99
|
+
let scenes = UIApplication.shared.connectedScenes
|
|
100
|
+
.compactMap { $0 as? UIWindowScene }
|
|
101
|
+
let window = scenes.flatMap { $0.windows }.first { $0.isKeyWindow }
|
|
102
|
+
var top = window?.rootViewController
|
|
103
|
+
while let presented = top?.presentedViewController {
|
|
104
|
+
top = presented
|
|
105
|
+
}
|
|
106
|
+
return top
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Delegate holder
|
|
111
|
+
|
|
112
|
+
private final class DigitalContractDelegateHolder: NSObject, VIDVDigitalContractDelegate {
|
|
113
|
+
var onResult: ((String) -> Void)?
|
|
114
|
+
var onError: ((String, String) -> Void)?
|
|
115
|
+
|
|
116
|
+
func onContractResult(_ result: ContractResult) {
|
|
117
|
+
switch result {
|
|
118
|
+
case .success(let data):
|
|
119
|
+
onResult?(jsonEnvelope(state: "SUCCESS", body: ["data": serializeData(data) as Any]))
|
|
120
|
+
|
|
121
|
+
case .builderError(let code, let message):
|
|
122
|
+
onResult?(jsonEnvelope(state: "ERROR", body: ["code": code, "message": message]))
|
|
123
|
+
|
|
124
|
+
case .serviceFailure(let message, let sdkError):
|
|
125
|
+
var body: [String: Any] = ["message": message]
|
|
126
|
+
if let sdkError = sdkError {
|
|
127
|
+
body["sdkError"] = [
|
|
128
|
+
"errorCode": sdkError.errorCode as Any,
|
|
129
|
+
"message": sdkError.message as Any,
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
onResult?(jsonEnvelope(state: "FAILURE", body: body))
|
|
133
|
+
|
|
134
|
+
case .exit(let data):
|
|
135
|
+
onResult?(jsonEnvelope(state: "EXIT", body: ["data": serializeData(data) as Any]))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func serializeData(_ data: VIDVDigitalContractResult?) -> [String: Any]? {
|
|
140
|
+
guard let data = data else { return nil }
|
|
141
|
+
var map: [String: Any] = [:]
|
|
142
|
+
if let contractId = data.contractId { map["contractID"] = contractId }
|
|
143
|
+
if let iframeURL = data.iframeURL { map["iframeURL"] = iframeURL }
|
|
144
|
+
if let contractStatus = data.contractStatus { map["contractStatus"] = contractStatus }
|
|
145
|
+
return map.isEmpty ? nil : map
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func jsonEnvelope(state: String, body: [String: Any]) -> String {
|
|
149
|
+
let payload: [String: Any] = ["state": state, "result": body]
|
|
150
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
151
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
152
|
+
return "{\"state\":\"\(state)\",\"result\":{}}"
|
|
153
|
+
}
|
|
154
|
+
return json
|
|
155
|
+
}
|
|
156
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@valifysolutions/react-native-vidvdigitalcontract",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "React Native plugin for the VIDV Digital Contract SDK",
|
|
5
|
+
"main": "src/index",
|
|
6
|
+
"types": "src/index",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"android",
|
|
10
|
+
"ios",
|
|
11
|
+
"vidvdigitalcontract-react-native.podspec",
|
|
12
|
+
"react-native.config.js",
|
|
13
|
+
"!android/build",
|
|
14
|
+
"!android/.gradle",
|
|
15
|
+
"!ios/build",
|
|
16
|
+
"!**/__tests__"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"example": "yarn --cwd example",
|
|
20
|
+
"typescript": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react-native",
|
|
24
|
+
"ios",
|
|
25
|
+
"android",
|
|
26
|
+
"vidv",
|
|
27
|
+
"digital-contract"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/valify/VIDVDigitalContract-ReactNative.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "Valify",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@babel/runtime": "^7.20.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18.0.0",
|
|
40
|
+
"react-native": ">=0.70.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/react": "^18.2.0",
|
|
44
|
+
"react": "18.2.0",
|
|
45
|
+
"react-native": "0.73.6",
|
|
46
|
+
"typescript": "^5.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
import type { DigitalContractResponse } from './types';
|
|
3
|
+
|
|
4
|
+
export * from './types';
|
|
5
|
+
|
|
6
|
+
const LINKING_ERROR =
|
|
7
|
+
`The package 'vidv-digital-contract-react-native' doesn't seem to be linked. Make sure: \n\n` +
|
|
8
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
9
|
+
'- You rebuilt the app after installing the package\n' +
|
|
10
|
+
'- You are not using Expo managed workflow\n';
|
|
11
|
+
|
|
12
|
+
const NativeVIDVDigitalContract = NativeModules.VIDVDigitalContract
|
|
13
|
+
? NativeModules.VIDVDigitalContract
|
|
14
|
+
: new Proxy(
|
|
15
|
+
{},
|
|
16
|
+
{
|
|
17
|
+
get() {
|
|
18
|
+
throw new Error(LINKING_ERROR);
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Thin React Native wrapper around the native VIDV Digital Contract SDK
|
|
25
|
+
* (iOS `VIDVDigitalContract`, Android `vidvdigitalcontractsdk`).
|
|
26
|
+
*
|
|
27
|
+
* The native SDKs present their own UI. This module just launches that flow
|
|
28
|
+
* with the given args map and returns the SDK's terminal result.
|
|
29
|
+
*
|
|
30
|
+
* Mirrors the Flutter plugin's `VIDVDigitalContract` class exactly.
|
|
31
|
+
*/
|
|
32
|
+
export class VIDVDigitalContract {
|
|
33
|
+
/**
|
|
34
|
+
* Starts the native Digital Contract flow described by {@link args} and
|
|
35
|
+
* resolves with the result as a JSON string,
|
|
36
|
+
* e.g. `{"state":"SUCCESS","result":{...}}`.
|
|
37
|
+
*
|
|
38
|
+
* `state` is one of `SUCCESS`, `FAILURE`, `EXIT`, or `ERROR`.
|
|
39
|
+
*
|
|
40
|
+
* Throws (rejects) only for pre-flight failures:
|
|
41
|
+
* - `ALREADY_RUNNING` — another start() call is already in progress
|
|
42
|
+
* - `NO_ACTIVITY` (Android) — not attached to an Activity
|
|
43
|
+
* - `NO_VIEW_CONTROLLER` (iOS) — root view controller not found
|
|
44
|
+
* - `START_ERROR` (Android) — exception while building the native flow
|
|
45
|
+
*/
|
|
46
|
+
async start(args: Record<string, unknown>): Promise<string | null> {
|
|
47
|
+
const json: string = await NativeVIDVDigitalContract.startDigitalContract(args);
|
|
48
|
+
return json ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convenience wrapper that parses the JSON string returned by {@link start}
|
|
53
|
+
* into a typed {@link DigitalContractResponse} object.
|
|
54
|
+
*/
|
|
55
|
+
async startParsed(args: Record<string, unknown>): Promise<DigitalContractResponse | null> {
|
|
56
|
+
const json = await this.start(args);
|
|
57
|
+
if (json == null) return null;
|
|
58
|
+
return JSON.parse(json) as DigitalContractResponse;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface DigitalContractData {
|
|
2
|
+
contractID?: string;
|
|
3
|
+
iframeURL?: string;
|
|
4
|
+
contractStatus?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type DigitalContractState = 'SUCCESS' | 'ERROR' | 'FAILURE' | 'EXIT';
|
|
8
|
+
|
|
9
|
+
export interface DigitalContractSuccessResult {
|
|
10
|
+
data?: DigitalContractData;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DigitalContractErrorResult {
|
|
14
|
+
code?: string | number;
|
|
15
|
+
message?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DigitalContractFailureResult {
|
|
19
|
+
code?: string | number;
|
|
20
|
+
message?: string;
|
|
21
|
+
data?: DigitalContractData;
|
|
22
|
+
sdkError?: {
|
|
23
|
+
errorCode?: string | number;
|
|
24
|
+
message?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DigitalContractExitResult {
|
|
29
|
+
step?: string;
|
|
30
|
+
data?: DigitalContractData;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DigitalContractResponse {
|
|
34
|
+
state: DigitalContractState;
|
|
35
|
+
result:
|
|
36
|
+
| DigitalContractSuccessResult
|
|
37
|
+
| DigitalContractErrorResult
|
|
38
|
+
| DigitalContractFailureResult
|
|
39
|
+
| DigitalContractExitResult;
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "vidvdigitalcontract-react-native"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["repository"]["url"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = { "Valify" => "info@valify.me" }
|
|
12
|
+
s.platforms = { :ios => "13.0" }
|
|
13
|
+
s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
|
|
14
|
+
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
16
|
+
|
|
17
|
+
s.dependency "React-Core"
|
|
18
|
+
s.dependency "VIDVDigitalContract", "~> 1.1.4"
|
|
19
|
+
|
|
20
|
+
s.swift_version = "5.0"
|
|
21
|
+
|
|
22
|
+
end
|