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 plugin for the Halo SDK, enabling Android NFC card-present payment transactions.
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
+ ![Halo Dot SDK Architecture](https://static.dev.haloplus.io/static/mpos/readme/assets/full_process_MIPS_1200.png)
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
- - React Native 0.73+
8
- - Android SDK 29+ (minSdkVersion 29)
9
- - NFC-capable Android device
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
- ## Installation
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
- ### 1. Install the package
53
+ Recommended libraries:
54
+ - [`react-native-permissions`](https://www.npmjs.com/package/react-native-permissions) `^4.0.0` — runtime permission handling
14
55
 
15
- Add the package to your project using npm or yarn:
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
+ ![access sdk](https://static.dev.haloplus.io/static/mpos/readme/assets/access_sdk.jpg)
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
+ ![public key](https://static.dev.haloplus.io/static/mpos/readme/assets/public_key.png)
71
+ 6. Copy your **Access Key** and **Secret Key** — you will need these to download the SDK from the Halo Maven repo
72
+ ![access key](https://static.dev.haloplus.io/static/mpos/readme/assets/access_key.png)
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
- npm install halo-sdk-react-native
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
- ### 2. Configure AWS credentials for the Halo Maven repository
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 fetches the Halo SDK from an S3-backed Maven repo. Provide credentials in `android/local.properties`:
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
- Or set environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
130
+ > **Important:** Never commit `local.properties` to source control. Add it to your `.gitignore`
36
131
 
37
- ### 3. Register the native module
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
- In your `MainApplication`, add `HaloSdkPackage` to `getPackages()`:
40
-
41
- ```kotlin
42
- override fun getPackages(): List<ReactPackage> = listOf(
43
- MainReactPackage(),
44
- HaloSdkPackage(),
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
- ### 4. Extend HaloReactActivity
144
+ ### Native Module Setup
49
145
 
50
- In `MainActivity`, extend `HaloReactActivity` instead of `ReactActivity`:
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
- override fun getMainComponentName(): String = "YourAppName"
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 ensures Halo SDK lifecycle management works correctly.
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
- const callbacks: IHaloCallbacks = {
77
- onInitializationResult(result: HaloInitializationResult) {
78
- console.log('Initialized:', result.resultType);
79
- },
80
- onHaloTransactionResult(result: HaloTransactionResult) {
81
- console.log('Transaction:', result.resultType, result.errorCode);
82
- },
83
- onHaloUIMessage(message: HaloUIMessage) {
84
- console.log('UI Message:', message.msgID);
85
- },
86
- onAttestationError(details: HaloAttestationHealthResult) {
87
- console.error('Attestation error:', details.errorCode);
88
- },
89
- onRequestJWT(jwtCallback: (jwt: string) => void) {
90
- const jwt = getJwtFromYourServer();
91
- jwtCallback(jwt);
92
- },
93
- onSecurityError(errorCode: string) {
94
- console.error('Security error:', errorCode);
95
- },
96
- onCameraControlLost() {
97
- console.warn('Camera control lost');
98
- },
99
- };
100
-
101
- // Initialize once (e.g. on app start)
102
- await HaloSdk.initialize(
103
- callbacks,
104
- 'com.your.package.name',
105
- '1.0.0',
106
- 300000, // transaction timeout in ms (optional, default 300000)
107
- true, // enable scheme animations (optional)
108
- );
109
-
110
- // Start a purchase
111
- const result = await HaloSdk.startTransaction(10.50, 'REF-001', 'ZAR');
112
-
113
- // Start a card refund
114
- const refund = await HaloSdk.cardRefundTransaction(10.50, 'REF-001', 'ZAR');
115
-
116
- // Cancel current transaction
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
- ## API
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` | Event handler object |
129
- | `applicationPackageName` | `string` | Your app's package name |
130
- | `applicationVersion` | `string` | Your app's version string |
131
- | `onStartTransactionTimeOut` | `number?` | Tap timeout in ms (default 300000) |
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. Resolves with a `HaloStartTransactionResult` once the card tap is accepted or rejected.
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 transaction.
648
+ Starts a card-present refund. Same parameters as `startTransaction`.
141
649
 
142
650
  ### `HaloSdk.cancelTransaction()`
143
651
 
144
- Requests cancellation of the current in-progress transaction.
652
+ Cancels the current in-progress transaction (e.g. if the user presses Cancel while waiting for a tap).
145
653
 
146
- ## Callbacks (`IHaloCallbacks`)
654
+ ### Callbacks (`IHaloCallbacks`)
147
655
 
148
- | Callback | Description |
149
- |----------------------------|-----------------------------------------------------------------------|
150
- | `onInitializationResult` | SDK initialisation complete or failed |
151
- | `onHaloTransactionResult` | Final transaction outcome |
152
- | `onHaloUIMessage` | Prompt to display to the cardholder (e.g. "Present card") |
153
- | `onRequestJWT` | SDK needs a fresh JWT; call the provided `jwtCallback` with the token |
154
- | `onAttestationError` | Device attestation failed |
155
- | `onSecurityError` | Security check failed |
156
- | `onCameraControlLost` | SDK released camera control |
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
- ## Building the plugin
666
+ ---
159
667
 
160
- ```bash
161
- npm install
162
- npm run build
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.
@@ -21,7 +21,7 @@ rootProject.allprojects {
21
21
  ]
22
22
 
23
23
  flatDir {
24
- dirs project(':halo_sdk_react_native_plugin').file('libs')
24
+ dirs "${project(':halo-sdk-react-native').projectDir}/libs"
25
25
  }
26
26
 
27
27
  repos.each { repo ->
@@ -24,17 +24,20 @@ class HaloSdkModule(reactContext: ReactApplicationContext) :
24
24
 
25
25
  @ReactMethod
26
26
  fun initializeHaloSDK(args: ReadableMap, promise: Promise) {
27
- haloSdkImplementation.initializeHaloSDK(promise, args.toHashMap())
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
- haloSdkImplementation.startTransaction(promise, args.toHashMap())
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
- haloSdkImplementation.startTransaction(promise, args.toHashMap(), TransactionType.Refund)
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-sdk-react-native",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "React Native plugin for Halo SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",