halo-sdk-react-native 1.0.0 → 1.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/README.md
CHANGED
|
@@ -1,163 +1,731 @@
|
|
|
1
1
|
# halo-sdk-react-native
|
|
2
2
|
|
|
3
|
-
React Native
|
|
3
|
+
A React Native implementation of the [Halo Dot SDK](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk/1.-getting-started).
|
|
4
|
+
|
|
5
|
+
The Halo Dot SDK is an Isolating MPoC SDK payment processing software with Attestation & Monitoring Capabilities. It turns an NFC-capable Android phone into a card-present payment terminal, no extra hardware required.
|
|
6
|
+
|
|
7
|
+
The diagram below shows the SDK boundary and how it interacts with your app, the cardholder's card, and the Halo payment gateway.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Requirements](#requirements)
|
|
14
|
+
- [Developer Portal Registration](#developer-portal-registration)
|
|
15
|
+
- [Getting Started](#getting-started)
|
|
16
|
+
- [Create a React Native App](#create-a-react-native-app)
|
|
17
|
+
- [Environment Setup](#environment-setup)
|
|
18
|
+
- [Plugin Installation](#plugin-installation)
|
|
19
|
+
- [Native Module Setup](#native-module-setup)
|
|
20
|
+
- [AndroidManifest Permissions](#androidmanifest-permissions)
|
|
21
|
+
- [JWT — What It Is and How to Set It Up](#jwt--what-it-is-and-how-to-set-it-up)
|
|
22
|
+
- [JWT Lifetime](#jwt-lifetime)
|
|
23
|
+
- [JWT Claims](#jwt-claims)
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Step 1 — Request Permissions](#step-1--request-permissions)
|
|
26
|
+
- [Step 2 — Set Up Callbacks](#step-2--set-up-callbacks)
|
|
27
|
+
- [Step 3 — Initialize the SDK](#step-3--initialize-the-sdk)
|
|
28
|
+
- [Step 4 — Run a Transaction](#step-4--run-a-transaction)
|
|
29
|
+
- [Full Example](#full-example)
|
|
30
|
+
- [API Reference](#api-reference)
|
|
31
|
+
- [Documentation](#documentation)
|
|
32
|
+
- [Testing](#testing)
|
|
33
|
+
- [FAQ](#faq)
|
|
34
|
+
|
|
35
|
+
---
|
|
4
36
|
|
|
5
37
|
## Requirements
|
|
6
38
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
39
|
+
| Requirement | Minimum |
|
|
40
|
+
|---|---|
|
|
41
|
+
| [React Native](https://reactnative.dev/) | 0.73+ |
|
|
42
|
+
| [Java](https://www.java.com/en/) | 21 |
|
|
43
|
+
| Android `minSdkVersion` | 29 |
|
|
44
|
+
| Android `compileSdkVersion` / `targetSdkVersion` | 34+ |
|
|
45
|
+
| Device | NFC-capable Android phone |
|
|
46
|
+
| IDE | [Android Studio](https://developer.android.com/studio/install) recommended |
|
|
10
47
|
|
|
11
|
-
|
|
48
|
+
You will also need:
|
|
49
|
+
- A developer account on the [Halo developer portal](https://go.developerportal.qa.haloplus.io/)
|
|
50
|
+
- A signed Non-Disclosure Agreement (NDA), available on the portal
|
|
51
|
+
- A JWT — generated via the developer portal or your own backend (see [JWT section](#jwt--what-it-is-and-how-to-set-it-up))
|
|
12
52
|
|
|
13
|
-
|
|
53
|
+
Recommended libraries:
|
|
54
|
+
- [`react-native-permissions`](https://www.npmjs.com/package/react-native-permissions) `^4.0.0` — runtime permission handling
|
|
14
55
|
|
|
15
|
-
|
|
56
|
+
> **Note:** Android is the only supported platform.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Developer Portal Registration
|
|
61
|
+
|
|
62
|
+
You must register on the QA (UAT) environment before going to production.
|
|
63
|
+
|
|
64
|
+
1. Go to [go.developerportal.qa.haloplus.io](https://go.developerportal.qa.haloplus.io/) and create an account
|
|
65
|
+
2. Verify your account via OTP
|
|
66
|
+
3. Click **Access the SDK**
|
|
67
|
+

|
|
68
|
+
4. Download and accept the NDA
|
|
69
|
+
5. Submit your RSA **public key** and create an **Issuer name** — these are used to verify the JWTs your app will sign
|
|
70
|
+

|
|
71
|
+
6. Copy your **Access Key** and **Secret Key** — you will need these to download the SDK from the Halo Maven repo
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Getting Started
|
|
77
|
+
|
|
78
|
+
### Create a React Native App
|
|
79
|
+
|
|
80
|
+
If you don't already have a React Native project, create one:
|
|
16
81
|
|
|
17
82
|
```bash
|
|
18
|
-
|
|
83
|
+
npx react-native init MyHaloApp --version 0.73.0
|
|
84
|
+
cd MyHaloApp
|
|
19
85
|
```
|
|
20
86
|
|
|
87
|
+
### Environment Setup
|
|
88
|
+
|
|
89
|
+
1. Make sure you have **Java 21** installed. Run `java -version` to check.
|
|
90
|
+
|
|
91
|
+
2. Open `android/app/build.gradle` and confirm `minSdkVersion` is `29` or higher:
|
|
92
|
+
|
|
93
|
+
```gradle
|
|
94
|
+
android {
|
|
95
|
+
defaultConfig {
|
|
96
|
+
applicationId "com.yourcompany.myapp"
|
|
97
|
+
minSdkVersion 29 // <-- must be 29+
|
|
98
|
+
targetSdkVersion 34
|
|
99
|
+
compileSdkVersion 34
|
|
100
|
+
// ...
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
3. See the [FAQ](#faq) if you have trouble with these SDK version settings.
|
|
106
|
+
|
|
107
|
+
### Plugin Installation
|
|
108
|
+
|
|
109
|
+
**1.** Install the npm package:
|
|
110
|
+
|
|
21
111
|
```bash
|
|
112
|
+
npm install halo-sdk-react-native
|
|
113
|
+
# or
|
|
22
114
|
yarn add halo-sdk-react-native
|
|
23
115
|
```
|
|
24
116
|
|
|
25
|
-
|
|
117
|
+
**2.** Install `react-native-permissions` (recommended for runtime permission handling):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install react-native-permissions
|
|
121
|
+
```
|
|
26
122
|
|
|
27
|
-
The plugin
|
|
123
|
+
**3.** The plugin downloads the Halo SDK binaries from an S3-backed Maven repo. Add your credentials (from the [developer portal](#developer-portal-registration)) to `android/local.properties` (create the file if it doesn't exist):
|
|
28
124
|
|
|
29
125
|
```properties
|
|
30
126
|
aws.accesskey=YOUR_ACCESS_KEY
|
|
31
127
|
aws.secretkey=YOUR_SECRET_KEY
|
|
32
|
-
aws.token=YOUR_SESSION_TOKEN # optional
|
|
33
128
|
```
|
|
34
129
|
|
|
35
|
-
|
|
130
|
+
> **Important:** Never commit `local.properties` to source control. Add it to your `.gitignore`
|
|
36
131
|
|
|
37
|
-
|
|
132
|
+
**4.** Add the following snippet to `android/app/build.gradle` so Gradle can read `local.properties` (it may already be there in newer RN templates):
|
|
38
133
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
134
|
+
```gradle
|
|
135
|
+
def localProperties = new Properties()
|
|
136
|
+
def localPropertiesFile = rootProject.file('local.properties')
|
|
137
|
+
if (localPropertiesFile.exists()) {
|
|
138
|
+
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
139
|
+
localProperties.load(reader)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
46
142
|
```
|
|
47
143
|
|
|
48
|
-
###
|
|
144
|
+
### Native Module Setup
|
|
49
145
|
|
|
50
|
-
|
|
146
|
+
**5.** Open `android/app/src/main/kotlin/.../MainActivity.kt` and extend `HaloReactActivity` instead of `ReactActivity`:
|
|
51
147
|
|
|
52
148
|
```kotlin
|
|
149
|
+
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
|
150
|
+
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
|
53
151
|
import za.co.synthesis.halo.sdkreactnativeplugin.HaloReactActivity
|
|
54
152
|
|
|
55
153
|
class MainActivity : HaloReactActivity() {
|
|
56
|
-
|
|
154
|
+
|
|
155
|
+
override fun getMainComponentName(): String = "MyHaloApp"
|
|
156
|
+
|
|
57
157
|
override fun createReactActivityDelegate() =
|
|
58
158
|
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
|
59
159
|
}
|
|
60
160
|
```
|
|
61
161
|
|
|
62
|
-
This
|
|
162
|
+
This replaces `ReactActivity` so that NFC foreground dispatch and the Halo SDK lifecycle are managed automatically.
|
|
163
|
+
|
|
164
|
+
**6.** Open `android/app/src/main/kotlin/.../MainApplication.kt` and register `HaloSdkPackage`:
|
|
165
|
+
|
|
166
|
+
```kotlin
|
|
167
|
+
import android.app.Application
|
|
168
|
+
import com.facebook.react.PackageList
|
|
169
|
+
import com.facebook.react.ReactApplication
|
|
170
|
+
import com.facebook.react.ReactNativeHost
|
|
171
|
+
import com.facebook.react.ReactPackage
|
|
172
|
+
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
173
|
+
import com.facebook.soloader.SoLoader
|
|
174
|
+
import za.co.synthesis.halo.sdkreactnativeplugin.HaloSdkPackage // <-- add this import
|
|
175
|
+
|
|
176
|
+
class MainApplication : Application(), ReactApplication {
|
|
177
|
+
|
|
178
|
+
override val reactNativeHost: ReactNativeHost =
|
|
179
|
+
object : DefaultReactNativeHost(this) {
|
|
180
|
+
override fun getPackages(): List<ReactPackage> =
|
|
181
|
+
PackageList(this).packages.apply {
|
|
182
|
+
add(HaloSdkPackage()) // <-- add this line
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
override fun getJSMainModuleName(): String = "index"
|
|
186
|
+
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
|
187
|
+
override val isNewArchEnabled: Boolean = false
|
|
188
|
+
override val isHermesEnabled: Boolean = true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override fun onCreate() {
|
|
192
|
+
super.onCreate()
|
|
193
|
+
SoLoader.init(this, false)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### AndroidManifest Permissions
|
|
199
|
+
|
|
200
|
+
**7.** Add the required permissions to `android/app/src/main/AndroidManifest.xml`:
|
|
201
|
+
|
|
202
|
+
```xml
|
|
203
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
204
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
205
|
+
|
|
206
|
+
<uses-feature
|
|
207
|
+
android:name="android.hardware.camera"
|
|
208
|
+
android:required="false" />
|
|
209
|
+
|
|
210
|
+
<!-- Bluetooth — legacy permissions for API 29/30 -->
|
|
211
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
212
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
213
|
+
|
|
214
|
+
<!-- Bluetooth — API 31+ -->
|
|
215
|
+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
|
216
|
+
android:usesPermissionFlags="neverForLocation" />
|
|
217
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
218
|
+
|
|
219
|
+
<!-- Location (required for Bluetooth LE scanning) -->
|
|
220
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
221
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
222
|
+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
223
|
+
|
|
224
|
+
<!-- Other -->
|
|
225
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
226
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
227
|
+
<uses-permission android:name="android.permission.NFC" />
|
|
228
|
+
|
|
229
|
+
<uses-feature android:name="android.hardware.nfc" android:required="true" />
|
|
230
|
+
|
|
231
|
+
<application ...>
|
|
232
|
+
<activity
|
|
233
|
+
android:name=".MainActivity"
|
|
234
|
+
...>
|
|
235
|
+
<intent-filter>
|
|
236
|
+
<action android:name="android.intent.action.MAIN" />
|
|
237
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
238
|
+
</intent-filter>
|
|
239
|
+
|
|
240
|
+
<!-- Required: NFC foreground dispatch -->
|
|
241
|
+
<intent-filter>
|
|
242
|
+
<action android:name="android.nfc.action.TECH_DISCOVERED" />
|
|
243
|
+
</intent-filter>
|
|
244
|
+
</activity>
|
|
245
|
+
</application>
|
|
246
|
+
|
|
247
|
+
</manifest>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## JWT — What It Is and How to Set It Up
|
|
253
|
+
|
|
254
|
+
Every call to the Halo SDK must include a valid JWT. Think of it as a short-lived, signed pass that proves your app is authorised to accept payments for a given merchant.
|
|
255
|
+
|
|
256
|
+
The JWT is issued either by the [developer portal](https://go.developerportal.qa.haloplus.io/) (for testing) or by your own backend server (for production). The portal verifies it using the RSA **public key** you submitted during registration.
|
|
257
|
+
|
|
258
|
+
> **For testing:** use the portal's **Generate Token** tool to get a pre-generated JWT. You do not need to implement signing yourself to get started.
|
|
259
|
+
|
|
260
|
+
**Recommended approach:** split credentials and JWT retrieval into two files.
|
|
261
|
+
|
|
262
|
+
**`src/config.ts`** — stores your app's settings (never commit real values to source control):
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Copy this file to config.ts and fill in your values.
|
|
266
|
+
// config.ts should be added to .gitignore.
|
|
267
|
+
export const Config = {
|
|
268
|
+
// Paste a JWT generated from the developer portal here for testing.
|
|
269
|
+
// In production, fetch this from your backend instead.
|
|
270
|
+
tempJwt: 'eyJ...',
|
|
271
|
+
|
|
272
|
+
// SDK initialisation options (all obtained from the developer portal)
|
|
273
|
+
applicationPackageName: 'com.yourcompany.myapp',
|
|
274
|
+
applicationVersion: '1.0.0',
|
|
275
|
+
onStartTransactionTimeOut: 300000, // ms before "tap card" times out
|
|
276
|
+
enableSchemeAnimations: true, // show Visa/Mastercard animations
|
|
277
|
+
} as const;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**`src/jwt/JwtToken.ts`** — supplies the JWT when the SDK asks for one:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { Config } from '../config';
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Returns the JWT for the Halo SDK's onRequestJWT callback.
|
|
287
|
+
*
|
|
288
|
+
* For testing: set Config.tempJwt to a token from the developer portal.
|
|
289
|
+
* For production: replace this with a fetch from your backend server.
|
|
290
|
+
*/
|
|
291
|
+
export function getJwt(): string {
|
|
292
|
+
if (Config.tempJwt) {
|
|
293
|
+
return Config.tempJwt;
|
|
294
|
+
}
|
|
295
|
+
throw new Error(
|
|
296
|
+
'No JWT configured. Set Config.tempJwt in src/config.ts, ' +
|
|
297
|
+
'or implement a backend fetch here.',
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
> **Note:** `getJwt` is synchronous here for simplicity. If you fetch the JWT from a backend, make it `async` and update the `onRequestJWT` callback accordingly.
|
|
303
|
+
|
|
304
|
+
### JWT Lifetime
|
|
305
|
+
|
|
306
|
+
Keep the lifetime short — 15 minutes is the recommended maximum. A stolen JWT can only be abused for as long as it remains valid.
|
|
307
|
+
|
|
308
|
+
### JWT Claims
|
|
309
|
+
|
|
310
|
+
| Field | Type | Description |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| `alg` | String | `RS512` — RSA + SHA-512. Asymmetric signing is required so the server can verify without being able to forge tokens. |
|
|
313
|
+
| `iss` | String | Your issuer name, registered on the developer portal (e.g. `authserver.haloplus.io`). |
|
|
314
|
+
| `sub` | String | Your merchant or application ID. |
|
|
315
|
+
| `aud` | String | The Halo kernel server URL (e.g. `kernelserver.qa.haloplus.io`). |
|
|
316
|
+
| `usr` | String | The signed-in operator/user identifier. |
|
|
317
|
+
| `iat` | NumericDate | UTC timestamp of when the token was created. |
|
|
318
|
+
| `exp` | NumericDate | UTC expiry timestamp. |
|
|
319
|
+
| `aud_fingerprints` | String | CSV of SHA-256 TLS fingerprints for the kernel server. Supports multiple values for certificate rotation. |
|
|
320
|
+
|
|
321
|
+
---
|
|
63
322
|
|
|
64
323
|
## Usage
|
|
65
324
|
|
|
325
|
+
### Step 1 — Request Permissions
|
|
326
|
+
|
|
327
|
+
Request the Android runtime permissions before initialising the SDK. Android 12+ (API 31+) uses new Bluetooth permission names.
|
|
328
|
+
|
|
66
329
|
```typescript
|
|
330
|
+
// src/permissions.ts
|
|
331
|
+
import { PermissionsAndroid, Platform } from 'react-native';
|
|
332
|
+
|
|
333
|
+
export async function requestHaloPermissions(): Promise<void> {
|
|
334
|
+
if (Platform.OS !== 'android') return;
|
|
335
|
+
|
|
336
|
+
const sdkVersion =
|
|
337
|
+
typeof Platform.Version === 'number'
|
|
338
|
+
? Platform.Version
|
|
339
|
+
: parseInt(Platform.Version, 10);
|
|
340
|
+
|
|
341
|
+
const permissions: string[] = [
|
|
342
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
343
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
if (sdkVersion >= 31) {
|
|
347
|
+
// Android 12+ Bluetooth permissions
|
|
348
|
+
permissions.push(
|
|
349
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
350
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await PermissionsAndroid.requestMultiple(permissions);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Step 2 — Set Up Callbacks
|
|
359
|
+
|
|
360
|
+
The SDK communicates back to your app through an `IHaloCallbacks` object you provide. Each callback corresponds to a different type of event.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// src/haloCallbacks.ts
|
|
67
364
|
import {
|
|
68
|
-
HaloSdk,
|
|
69
365
|
type IHaloCallbacks,
|
|
366
|
+
type HaloAttestationHealthResult,
|
|
70
367
|
type HaloInitializationResult,
|
|
71
368
|
type HaloTransactionResult,
|
|
72
369
|
type HaloUIMessage,
|
|
73
|
-
type HaloAttestationHealthResult,
|
|
74
370
|
} from 'halo-sdk-react-native';
|
|
371
|
+
import { getJwt } from './jwt/JwtToken';
|
|
372
|
+
|
|
373
|
+
export function buildCallbacks(options: {
|
|
374
|
+
onStatusChange: (msg: string) => void;
|
|
375
|
+
onError: (msg: string) => void;
|
|
376
|
+
onTransactionResult: (result: HaloTransactionResult) => void;
|
|
377
|
+
}): IHaloCallbacks {
|
|
378
|
+
return {
|
|
379
|
+
// Called when the SDK finishes starting up
|
|
380
|
+
onInitializationResult(result: HaloInitializationResult) {
|
|
381
|
+
if (result.resultType === 'success') {
|
|
382
|
+
options.onStatusChange('SDK ready — present a card to pay');
|
|
383
|
+
} else {
|
|
384
|
+
options.onError(`Initialisation failed: ${result.errorCode}`);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// Called when a card tap completes (approved, declined, or error)
|
|
389
|
+
onHaloTransactionResult(result: HaloTransactionResult) {
|
|
390
|
+
options.onTransactionResult(result);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
// Called repeatedly during a transaction to tell you what to show the user
|
|
394
|
+
// e.g. "PRESENT_CARD", "PROCESSING", "APPROVED"
|
|
395
|
+
onHaloUIMessage(message: HaloUIMessage) {
|
|
396
|
+
options.onStatusChange(message.msgID);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
// The SDK asks you for a fresh JWT whenever it needs one
|
|
400
|
+
onRequestJWT(jwtCallback: (jwt: string) => void) {
|
|
401
|
+
try {
|
|
402
|
+
jwtCallback(getJwt());
|
|
403
|
+
} catch (err: any) {
|
|
404
|
+
options.onError(`JWT error: ${err.message}`);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
// Device failed attestation (tampered OS, emulator, etc.)
|
|
409
|
+
onAttestationError(details: HaloAttestationHealthResult) {
|
|
410
|
+
options.onError(`Attestation error: ${details.errorCode}`);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
// A security check failed (e.g. invalid JWT, revoked merchant)
|
|
414
|
+
onSecurityError(errorCode: string) {
|
|
415
|
+
options.onError(`Security error: ${errorCode}`);
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
// The SDK released the camera (e.g. card scheme animation finished)
|
|
419
|
+
onCameraControlLost() {
|
|
420
|
+
console.log('Camera released by SDK');
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Step 3 — Initialize the SDK
|
|
75
427
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
428
|
+
Call `HaloSdk.initialize` once, before running any transactions. A good place is in a `useEffect` when your payment screen mounts.
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { HaloSdk } from 'halo-sdk-react-native';
|
|
432
|
+
import { Config } from './config';
|
|
433
|
+
import { requestHaloPermissions } from './permissions';
|
|
434
|
+
import { buildCallbacks } from './haloCallbacks';
|
|
435
|
+
|
|
436
|
+
async function setupSdk(
|
|
437
|
+
onStatusChange: (msg: string) => void,
|
|
438
|
+
onError: (msg: string) => void,
|
|
439
|
+
onTransactionResult: (result: any) => void,
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
// 1. Request permissions first
|
|
442
|
+
await requestHaloPermissions();
|
|
443
|
+
|
|
444
|
+
// 2. Build your callbacks
|
|
445
|
+
const callbacks = buildCallbacks({ onStatusChange, onError, onTransactionResult });
|
|
446
|
+
|
|
447
|
+
// 3. Initialise — the SDK will call onInitializationResult when ready
|
|
448
|
+
await HaloSdk.initialize(
|
|
449
|
+
callbacks,
|
|
450
|
+
Config.applicationPackageName, // e.g. 'com.yourcompany.myapp'
|
|
451
|
+
Config.applicationVersion, // e.g. '1.0.0'
|
|
452
|
+
Config.onStartTransactionTimeOut, // ms before a tap times out (default 300000)
|
|
453
|
+
Config.enableSchemeAnimations, // show Visa/Mastercard animations
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Step 4 — Run a Transaction
|
|
459
|
+
|
|
460
|
+
Once the SDK is initialised, you can charge a card:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// Charge R 10.50
|
|
464
|
+
const result = await HaloSdk.startTransaction(10.50, 'ORDER-001', 'ZAR');
|
|
465
|
+
console.log(result.resultType); // e.g. "tap_started"
|
|
466
|
+
|
|
467
|
+
// Refund R 10.50 to the original card
|
|
468
|
+
const refund = await HaloSdk.cardRefundTransaction(10.50, 'ORDER-001', 'ZAR');
|
|
469
|
+
|
|
470
|
+
// Cancel a transaction that is waiting for a tap
|
|
117
471
|
await HaloSdk.cancelTransaction();
|
|
118
472
|
```
|
|
119
473
|
|
|
120
|
-
|
|
474
|
+
`startTransaction` and `cardRefundTransaction` resolve immediately once the tap is registered — the final payment outcome arrives through `onHaloTransactionResult`.
|
|
475
|
+
|
|
476
|
+
### Full Example
|
|
477
|
+
|
|
478
|
+
Below is a minimal but complete payment screen taken directly from the example app in this repo:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
// App.tsx
|
|
482
|
+
import React, { useEffect, useState } from 'react';
|
|
483
|
+
import {
|
|
484
|
+
ActivityIndicator,
|
|
485
|
+
PermissionsAndroid,
|
|
486
|
+
Platform,
|
|
487
|
+
Text,
|
|
488
|
+
TextInput,
|
|
489
|
+
TouchableOpacity,
|
|
490
|
+
View,
|
|
491
|
+
} from 'react-native';
|
|
492
|
+
import {
|
|
493
|
+
HaloSdk,
|
|
494
|
+
type HaloTransactionResult,
|
|
495
|
+
type IHaloCallbacks,
|
|
496
|
+
} from 'halo-sdk-react-native';
|
|
497
|
+
import { getJwt } from './src/jwt/JwtToken';
|
|
498
|
+
import { Config } from './src/config';
|
|
499
|
+
|
|
500
|
+
export default function App() {
|
|
501
|
+
const [amount, setAmount] = useState('');
|
|
502
|
+
const [merchantRef, setMerchantRef] = useState('');
|
|
503
|
+
const [status, setStatus] = useState('Initialising...');
|
|
504
|
+
const [isReady, setIsReady] = useState(false);
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
initSdk();
|
|
508
|
+
}, []);
|
|
509
|
+
|
|
510
|
+
async function initSdk() {
|
|
511
|
+
// Request permissions
|
|
512
|
+
if (Platform.OS === 'android') {
|
|
513
|
+
const sdkVersion =
|
|
514
|
+
typeof Platform.Version === 'number'
|
|
515
|
+
? Platform.Version
|
|
516
|
+
: parseInt(Platform.Version, 10);
|
|
517
|
+
|
|
518
|
+
const perms: string[] = [
|
|
519
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
520
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
521
|
+
];
|
|
522
|
+
if (sdkVersion >= 31) {
|
|
523
|
+
perms.push(
|
|
524
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
525
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
await PermissionsAndroid.requestMultiple(perms);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const callbacks: IHaloCallbacks = {
|
|
532
|
+
onInitializationResult(result) {
|
|
533
|
+
setIsReady(result.resultType === 'success');
|
|
534
|
+
setStatus(
|
|
535
|
+
result.resultType === 'success'
|
|
536
|
+
? 'Ready — enter amount and tap Charge'
|
|
537
|
+
: `Init failed: ${result.errorCode}`,
|
|
538
|
+
);
|
|
539
|
+
},
|
|
540
|
+
onHaloTransactionResult(result: HaloTransactionResult) {
|
|
541
|
+
setStatus(`Result: ${result.resultType} ${result.errorCode}`);
|
|
542
|
+
},
|
|
543
|
+
onHaloUIMessage(message) {
|
|
544
|
+
// The SDK sends messages like "PRESENT_CARD", "PROCESSING", "APPROVED"
|
|
545
|
+
setStatus(message.msgID);
|
|
546
|
+
},
|
|
547
|
+
onRequestJWT(jwtCallback) {
|
|
548
|
+
try {
|
|
549
|
+
jwtCallback(getJwt());
|
|
550
|
+
} catch (e: any) {
|
|
551
|
+
setStatus(`JWT error: ${e.message}`);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
onAttestationError(details) {
|
|
555
|
+
setStatus(`Attestation error: ${details.errorCode}`);
|
|
556
|
+
},
|
|
557
|
+
onSecurityError(errorCode) {
|
|
558
|
+
setStatus(`Security error: ${errorCode}`);
|
|
559
|
+
},
|
|
560
|
+
onCameraControlLost() {},
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
HaloSdk.initialize(
|
|
564
|
+
callbacks,
|
|
565
|
+
Config.applicationPackageName,
|
|
566
|
+
Config.applicationVersion,
|
|
567
|
+
Config.onStartTransactionTimeOut,
|
|
568
|
+
Config.enableSchemeAnimations,
|
|
569
|
+
).catch(e => setStatus(`SDK error: ${e.message}`));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function charge() {
|
|
573
|
+
if (!amount || !merchantRef) {
|
|
574
|
+
setStatus('Please enter amount and merchant reference');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const result = await HaloSdk.startTransaction(parseFloat(amount), merchantRef, 'ZAR');
|
|
579
|
+
setStatus(`Tap accepted: ${result.resultType}`);
|
|
580
|
+
} catch (e: any) {
|
|
581
|
+
setStatus(`Error: ${e.message}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<View style={{ flex: 1, padding: 24, justifyContent: 'center' }}>
|
|
587
|
+
<Text style={{ fontSize: 13, color: '#555', marginBottom: 16 }}>{status}</Text>
|
|
588
|
+
|
|
589
|
+
{!isReady ? (
|
|
590
|
+
<ActivityIndicator size="large" />
|
|
591
|
+
) : (
|
|
592
|
+
<>
|
|
593
|
+
<TextInput
|
|
594
|
+
placeholder="Amount (e.g. 10.50)"
|
|
595
|
+
value={amount}
|
|
596
|
+
onChangeText={setAmount}
|
|
597
|
+
keyboardType="decimal-pad"
|
|
598
|
+
style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 8, borderRadius: 4 }}
|
|
599
|
+
/>
|
|
600
|
+
<TextInput
|
|
601
|
+
placeholder="Merchant reference (e.g. ORDER-001)"
|
|
602
|
+
value={merchantRef}
|
|
603
|
+
onChangeText={setMerchantRef}
|
|
604
|
+
style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 16, borderRadius: 4 }}
|
|
605
|
+
/>
|
|
606
|
+
<TouchableOpacity
|
|
607
|
+
onPress={charge}
|
|
608
|
+
style={{ backgroundColor: '#1976D2', padding: 14, borderRadius: 8, alignItems: 'center' }}>
|
|
609
|
+
<Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>Charge</Text>
|
|
610
|
+
</TouchableOpacity>
|
|
611
|
+
</>
|
|
612
|
+
)}
|
|
613
|
+
</View>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## API Reference
|
|
121
621
|
|
|
122
622
|
### `HaloSdk.initialize(callbacks, packageName, version, timeout?, animations?)`
|
|
123
623
|
|
|
124
624
|
Initialises the SDK. Must be called before any transaction methods.
|
|
125
625
|
|
|
126
|
-
| Parameter | Type | Description |
|
|
127
|
-
|
|
128
|
-
| `callbacks` | `IHaloCallbacks` |
|
|
129
|
-
| `applicationPackageName` | `string` | Your app's package name |
|
|
130
|
-
| `applicationVersion` | `string` | Your app's version string |
|
|
131
|
-
| `onStartTransactionTimeOut` | `number?` |
|
|
132
|
-
| `enableSchemeAnimations` | `boolean?` | Show Visa/Mastercard/Amex animations on approval |
|
|
626
|
+
| Parameter | Type | Default | Description |
|
|
627
|
+
|---|---|---|---|
|
|
628
|
+
| `callbacks` | `IHaloCallbacks` | — | Your callback handler object |
|
|
629
|
+
| `applicationPackageName` | `string` | — | Your app's Android package name |
|
|
630
|
+
| `applicationVersion` | `string` | — | Your app's version string |
|
|
631
|
+
| `onStartTransactionTimeOut` | `number?` | `300000` | Time in ms to wait for a card tap |
|
|
632
|
+
| `enableSchemeAnimations` | `boolean?` | `false` | Show Visa/Mastercard/Amex animations on approval |
|
|
133
633
|
|
|
134
634
|
### `HaloSdk.startTransaction(amount, reference, currency)`
|
|
135
635
|
|
|
136
|
-
Starts a purchase transaction.
|
|
636
|
+
Starts a purchase transaction. Prompts the user to tap their card.
|
|
637
|
+
|
|
638
|
+
| Parameter | Type | Example |
|
|
639
|
+
|---|---|---|
|
|
640
|
+
| `transactionAmount` | `number` | `10.50` |
|
|
641
|
+
| `merchantTransactionReference` | `string` | `'ORDER-001'` |
|
|
642
|
+
| `transactionCurrency` | `string` | `'ZAR'` |
|
|
643
|
+
|
|
644
|
+
Returns `Promise<HaloStartTransactionResult>` — resolves when the card tap is registered. The final outcome arrives via `onHaloTransactionResult`.
|
|
137
645
|
|
|
138
646
|
### `HaloSdk.cardRefundTransaction(amount, reference, currency)`
|
|
139
647
|
|
|
140
|
-
Starts a card-present refund
|
|
648
|
+
Starts a card-present refund. Same parameters as `startTransaction`.
|
|
141
649
|
|
|
142
650
|
### `HaloSdk.cancelTransaction()`
|
|
143
651
|
|
|
144
|
-
|
|
652
|
+
Cancels the current in-progress transaction (e.g. if the user presses Cancel while waiting for a tap).
|
|
145
653
|
|
|
146
|
-
|
|
654
|
+
### Callbacks (`IHaloCallbacks`)
|
|
147
655
|
|
|
148
|
-
| Callback
|
|
149
|
-
|
|
150
|
-
| `onInitializationResult`
|
|
151
|
-
| `onHaloTransactionResult`
|
|
152
|
-
| `onHaloUIMessage`
|
|
153
|
-
| `onRequestJWT`
|
|
154
|
-
| `onAttestationError`
|
|
155
|
-
| `onSecurityError`
|
|
156
|
-
| `onCameraControlLost`
|
|
656
|
+
| Callback | When it fires |
|
|
657
|
+
|---|---|
|
|
658
|
+
| `onInitializationResult(result)` | SDK startup complete (success or failure) |
|
|
659
|
+
| `onHaloTransactionResult(result)` | Final payment outcome — approved, declined, or error |
|
|
660
|
+
| `onHaloUIMessage(message)` | Prompt to show the user during a tap: `PRESENT_CARD`, `PROCESSING`, `APPROVED`, etc. |
|
|
661
|
+
| `onRequestJWT(jwtCallback)` | SDK needs a fresh JWT — call `jwtCallback(yourJwtString)` |
|
|
662
|
+
| `onAttestationError(details)` | Device integrity check failed (rooted device, emulator, etc.) |
|
|
663
|
+
| `onSecurityError(errorCode)` | JWT invalid, merchant revoked, or other security failure |
|
|
664
|
+
| `onCameraControlLost()` | SDK has finished using the camera |
|
|
157
665
|
|
|
158
|
-
|
|
666
|
+
---
|
|
159
667
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
668
|
+
## Documentation
|
|
669
|
+
|
|
670
|
+
Full SDK documentation: [halo-dot-developer-docs.gitbook.io](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk)
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Testing
|
|
675
|
+
|
|
676
|
+
All transactions are void until your NDA is fully executed.
|
|
677
|
+
|
|
678
|
+
You can simulate card taps using a virtual NFC card app such as [Visa Mobile CDET](https://apkpure.com/visa-mobile-cdet/com.visa.app.cdet) on a second Android device.
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## FAQ
|
|
683
|
+
|
|
684
|
+
**Q: How do I set `compileSdkVersion` / `minSdkVersion` if they're not set or causing issues?**
|
|
685
|
+
|
|
686
|
+
You can define them in `android/local.properties` and read them in Gradle:
|
|
687
|
+
|
|
688
|
+
```properties
|
|
689
|
+
sdk.dir=/Users/yourname/Library/Android/sdk
|
|
690
|
+
aws.accesskey=YOUR_ACCESS_KEY
|
|
691
|
+
aws.secretkey=YOUR_SECRET_KEY
|
|
692
|
+
compileSdkVersion=34
|
|
693
|
+
minSdkVersion=29
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
```gradle
|
|
697
|
+
// android/app/build.gradle
|
|
698
|
+
compileSdkVersion localProperties.getProperty('compileSdkVersion').toInteger()
|
|
163
699
|
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
**Q: The SDK fails to download / Gradle sync fails.**
|
|
704
|
+
|
|
705
|
+
- Confirm `aws.accesskey` and `aws.secretkey` are in `android/local.properties` with the exact casing shown
|
|
706
|
+
- Open the `android` folder in Android Studio and run **File → Sync Project with Gradle Files**
|
|
707
|
+
- Make sure you accepted the NDA on the developer portal (access is blocked until the NDA is signed)
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
**Q: I get a build error about `HaloReactActivity` or `HaloSdkPackage` not found.**
|
|
712
|
+
|
|
713
|
+
- Confirm the npm package is installed: `npm install halo-sdk-react-native`
|
|
714
|
+
- Confirm `HaloSdkPackage()` is added in `MainApplication.kt`
|
|
715
|
+
- Confirm `MainActivity` extends `HaloReactActivity` (not `ReactActivity`)
|
|
716
|
+
- Run a Gradle sync in Android Studio
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
**Q: The SDK initialises but transactions never complete.**
|
|
721
|
+
|
|
722
|
+
- Check that all [AndroidManifest permissions](#androidmanifest-permissions) are declared, especially the NFC `intent-filter` block
|
|
723
|
+
- Check that the NFC intent-filter is inside the `<activity>` block for `MainActivity`
|
|
724
|
+
- Verify your JWT is valid using `POST https://kernelserver.qa.haloplus.io/<sdk-version>/tokens/checkjwt`
|
|
725
|
+
- Check that `minSdkVersion` ≥ 29 and `compileSdkVersion` / `targetSdkVersion` ≥ 34
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
**Q: How do I get a test JWT quickly without implementing RS512 signing?**
|
|
730
|
+
|
|
731
|
+
Log into the [developer portal](https://go.developerportal.qa.haloplus.io/), navigate to your app, and use the portal's **Generate Token** tool. Paste the result into `Config.tempJwt` in your `config.ts`. Remember to implement proper signing before going to production.
|
package/android/build.gradle
CHANGED
|
@@ -24,17 +24,20 @@ class HaloSdkModule(reactContext: ReactApplicationContext) :
|
|
|
24
24
|
|
|
25
25
|
@ReactMethod
|
|
26
26
|
fun initializeHaloSDK(args: ReadableMap, promise: Promise) {
|
|
27
|
-
|
|
27
|
+
@Suppress("UNCHECKED_CAST")
|
|
28
|
+
haloSdkImplementation.initializeHaloSDK(promise, args.toHashMap() as HashMap<String, Any>)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
@ReactMethod
|
|
31
32
|
fun startTransaction(args: ReadableMap, promise: Promise) {
|
|
32
|
-
|
|
33
|
+
@Suppress("UNCHECKED_CAST")
|
|
34
|
+
haloSdkImplementation.startTransaction(promise, args.toHashMap() as HashMap<String, Any>)
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
@ReactMethod
|
|
36
38
|
fun cardRefundTransaction(args: ReadableMap, promise: Promise) {
|
|
37
|
-
|
|
39
|
+
@Suppress("UNCHECKED_CAST")
|
|
40
|
+
haloSdkImplementation.startTransaction(promise, args.toHashMap() as HashMap<String, Any>, TransactionType.Refund)
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
@ReactMethod
|
|
@@ -57,7 +60,7 @@ class HaloSdkModule(reactContext: ReactApplicationContext) :
|
|
|
57
60
|
// Keep UIContext activity reference up to date
|
|
58
61
|
override fun onHostResume() {
|
|
59
62
|
Log.d(TAG, "onHostResume")
|
|
60
|
-
UIContext.updateActivity(currentActivity)
|
|
63
|
+
UIContext.updateActivity(reactApplicationContext.currentActivity)
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
override fun onHostPause() {
|