halo-sdk-react-native 1.0.0 → 1.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to `halo-sdk-react-native` will be documented here.
4
+
5
+ ---
6
+
7
+ ## [1.0.2] - 2026-03-03
8
+
9
+ ### Fixed
10
+ - Changed Halo SDK dependency declaration from `compileOnly` to `api` in `build.gradle` so it is exposed transitively to host apps. This prevents a `NoClassDefFoundError: HaloSDK` crash on launch, host apps no longer need to add the Halo SDK as a direct dependency.
11
+
12
+ ### Documentation
13
+ - Updated README to remove the now-unnecessary manual Halo SDK runtime dependency step
14
+ - Fixed incorrect `resultType` check in code examples (`'success'`-> `'Initialized'`)
15
+ - Added Result Types reference table for `HaloInitializationResult` and `HaloTransactionResult`
16
+ - Added FAQ entries for common setup issues (Metro bundler `SyntaxError`, `JWTExpired` on init)
17
+ - Corrected the JWT section to reference official Halo documentation
18
+ - Fixed Full Example to use `requestHaloPermissions()` from `src/permissions.ts`
19
+
20
+ ---
21
+
22
+ ## [1.0.1] - Initial release
23
+
24
+ - Initial React Native plugin wrapping the Halo Dot Android SDK
25
+ - Supports `initialize`, `startTransaction`, `cardRefundTransaction`, and `cancelTransaction`
26
+ - Event-driven callbacks via `NativeEventEmitter`: initialization, transaction result, UI messages, JWT requests, attestation errors, security errors, and camera events
27
+ - Scheme animations (Visa / Mastercard / Amex) on approved transactions via `AnimationActivity`
package/README.md CHANGED
@@ -1,163 +1,712 @@
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
+ - [Usage](#usage)
23
+ - [Step 1 — Request Permissions](#step-1--request-permissions)
24
+ - [Step 2 — Set Up Callbacks](#step-2--set-up-callbacks)
25
+ - [Step 3 — Initialize the SDK](#step-3--initialize-the-sdk)
26
+ - [Step 4 — Run a Transaction](#step-4--run-a-transaction)
27
+ - [Full Example](#full-example)
28
+ - [API Reference](#api-reference)
29
+ - [Result Types](#result-types)
30
+ - [Documentation](#documentation)
31
+ - [Testing](#testing)
32
+ - [FAQ](#faq)
33
+
34
+ ---
4
35
 
5
36
  ## Requirements
6
37
 
7
- - React Native 0.73+
8
- - Android SDK 29+ (minSdkVersion 29)
9
- - NFC-capable Android device
38
+ | Requirement | Minimum |
39
+ |---|---|
40
+ | [React Native](https://reactnative.dev/) | 0.73+ |
41
+ | [Java](https://www.java.com/en/) | 21 |
42
+ | Android `minSdkVersion` | 29 |
43
+ | Android `compileSdkVersion` / `targetSdkVersion` | 34+ |
44
+ | Device | NFC-capable Android phone |
45
+ | IDE | [Android Studio](https://developer.android.com/studio/install) recommended |
10
46
 
11
- ## Installation
47
+ You will also need:
48
+ - A developer account on the [Halo developer portal](https://go.developerportal.qa.haloplus.io/)
49
+ - A signed Non-Disclosure Agreement (NDA), available on the portal
50
+ - 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
51
 
13
- ### 1. Install the package
52
+ > **Note:** Android is the only supported platform for now.
14
53
 
15
- Add the package to your project using npm or yarn:
54
+ ---
55
+
56
+ ## Developer Portal Registration
57
+
58
+ You must register on the QA (UAT) environment before going to production.
59
+
60
+ 1. Go to [go.developerportal.qa.haloplus.io](https://go.developerportal.qa.haloplus.io/) and create an account
61
+ 2. Verify your account via OTP
62
+ 3. Click **Access the SDK**
63
+ ![access sdk](https://static.dev.haloplus.io/static/mpos/readme/assets/access_sdk.jpg)
64
+ 4. Download and accept the NDA
65
+ 5. Submit your RSA **public key** and create an **Issuer name** — these are used to verify the JWTs your app will sign
66
+ ![public key](https://static.dev.haloplus.io/static/mpos/readme/assets/public_key.png)
67
+ 6. Copy your **Access Key** and **Secret Key** — you will need these to download the SDK from the Halo Maven repo
68
+ ![access key](https://static.dev.haloplus.io/static/mpos/readme/assets/access_key.png)
69
+
70
+ ---
71
+
72
+ ## Getting Started
73
+
74
+ ### Create a React Native App
75
+
76
+ If you don't already have a React Native project, create one:
16
77
 
17
78
  ```bash
18
- npm install halo-sdk-react-native
79
+ npx react-native init MyHaloApp
80
+ cd MyHaloApp
81
+ ```
82
+
83
+ ### Environment Setup
84
+
85
+ 1. Make sure you have **Java 21** installed. Run `java -version` to check.
86
+
87
+ 2. Open `android/app/build.gradle` and confirm `minSdkVersion` is `29` or higher:
88
+
89
+ ```gradle
90
+ android {
91
+ defaultConfig {
92
+ applicationId "com.yourcompany.myapp"
93
+ minSdkVersion 29 // <-- must be 29+
94
+ targetSdkVersion 34
95
+ compileSdkVersion 34
96
+ // ...
97
+ }
98
+ }
19
99
  ```
20
100
 
101
+ 3. See the [FAQ](#faq) if you have trouble with these SDK version settings.
102
+
103
+ ### Plugin Installation
104
+
105
+ **1.** Install the npm package:
106
+
21
107
  ```bash
108
+ npm install halo-sdk-react-native
109
+ # or
22
110
  yarn add halo-sdk-react-native
23
111
  ```
24
112
 
25
- ### 2. Configure AWS credentials for the Halo Maven repository
26
-
27
- The plugin fetches the Halo SDK from an S3-backed Maven repo. Provide credentials in `android/local.properties`:
113
+ **2.** 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
114
 
29
115
  ```properties
30
116
  aws.accesskey=YOUR_ACCESS_KEY
31
117
  aws.secretkey=YOUR_SECRET_KEY
32
- aws.token=YOUR_SESSION_TOKEN # optional
33
118
  ```
34
119
 
35
- Or set environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
36
-
37
- ### 3. Register the native module
120
+ > **Important:** Never commit `local.properties` to source control. Add it to your `.gitignore`
38
121
 
39
- In your `MainApplication`, add `HaloSdkPackage` to `getPackages()`:
122
+ **3.** Add the following snippet to `android/app/build.gradle` so Gradle can read `local.properties` (it may already be there in newer RN templates):
40
123
 
41
- ```kotlin
42
- override fun getPackages(): List<ReactPackage> = listOf(
43
- MainReactPackage(),
44
- HaloSdkPackage(),
45
- )
124
+ ```gradle
125
+ def localProperties = new Properties()
126
+ def localPropertiesFile = rootProject.file('local.properties')
127
+ if (localPropertiesFile.exists()) {
128
+ localPropertiesFile.withReader('UTF-8') { reader ->
129
+ localProperties.load(reader)
130
+ }
131
+ }
46
132
  ```
47
133
 
48
- ### 4. Extend HaloReactActivity
134
+ ### Native Module Setup
49
135
 
50
- In `MainActivity`, extend `HaloReactActivity` instead of `ReactActivity`:
136
+ **4.** Open `android/app/src/main/kotlin/.../MainActivity.kt` and extend `HaloReactActivity` instead of `ReactActivity`:
51
137
 
52
138
  ```kotlin
139
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
140
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
53
141
  import za.co.synthesis.halo.sdkreactnativeplugin.HaloReactActivity
54
142
 
55
143
  class MainActivity : HaloReactActivity() {
56
- override fun getMainComponentName(): String = "YourAppName"
144
+
145
+ override fun getMainComponentName(): String = "MyHaloApp"
146
+
57
147
  override fun createReactActivityDelegate() =
58
148
  DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
59
149
  }
60
150
  ```
61
151
 
62
- This ensures Halo SDK lifecycle management works correctly.
152
+ This replaces `ReactActivity` so that NFC foreground dispatch and the Halo SDK lifecycle are managed automatically.
153
+
154
+ **5.** Open `android/app/src/main/kotlin/.../MainApplication.kt` and register `HaloSdkPackage`:
155
+
156
+ ```kotlin
157
+ import android.app.Application
158
+ import com.facebook.react.PackageList
159
+ import com.facebook.react.ReactApplication
160
+ import com.facebook.react.ReactNativeHost
161
+ import com.facebook.react.ReactPackage
162
+ import com.facebook.react.defaults.DefaultReactNativeHost
163
+ import com.facebook.soloader.SoLoader
164
+ import za.co.synthesis.halo.sdkreactnativeplugin.HaloSdkPackage // <-- add this import
165
+
166
+ class MainApplication : Application(), ReactApplication {
167
+
168
+ override val reactNativeHost: ReactNativeHost =
169
+ object : DefaultReactNativeHost(this) {
170
+ override fun getPackages(): List<ReactPackage> =
171
+ PackageList(this).packages.apply {
172
+ add(HaloSdkPackage()) // <-- add this line
173
+ }
174
+
175
+ override fun getJSMainModuleName(): String = "index"
176
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
177
+ override val isNewArchEnabled: Boolean = false
178
+ override val isHermesEnabled: Boolean = true
179
+ }
180
+
181
+ override fun onCreate() {
182
+ super.onCreate()
183
+ SoLoader.init(this, false)
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### AndroidManifest Permissions
189
+
190
+ **6.** Add the required permissions to `android/app/src/main/AndroidManifest.xml`:
191
+
192
+ ```xml
193
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
194
+ xmlns:tools="http://schemas.android.com/tools">
195
+
196
+ <uses-feature
197
+ android:name="android.hardware.camera"
198
+ android:required="false" />
199
+
200
+ <!-- Bluetooth — legacy permissions for API 29/30 -->
201
+ <uses-permission android:name="android.permission.BLUETOOTH" />
202
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
203
+
204
+ <!-- Bluetooth — API 31+ -->
205
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
206
+ android:usesPermissionFlags="neverForLocation" />
207
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
208
+
209
+ <!-- Location (required for Bluetooth LE scanning) -->
210
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
211
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
212
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
213
+
214
+ <!-- Other -->
215
+ <uses-permission android:name="android.permission.INTERNET" />
216
+ <uses-permission android:name="android.permission.CAMERA" />
217
+ <uses-permission android:name="android.permission.NFC" />
218
+
219
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
220
+
221
+ <application ...>
222
+ <activity
223
+ android:name=".MainActivity"
224
+ ...>
225
+ <intent-filter>
226
+ <action android:name="android.intent.action.MAIN" />
227
+ <category android:name="android.intent.category.LAUNCHER" />
228
+ </intent-filter>
229
+
230
+ <!-- Required: NFC foreground dispatch -->
231
+ <intent-filter>
232
+ <action android:name="android.nfc.action.TECH_DISCOVERED" />
233
+ </intent-filter>
234
+ </activity>
235
+ </application>
236
+
237
+ </manifest>
238
+ ```
239
+
240
+ ---
241
+
242
+ ## JWT — What It Is and How to Set It Up
243
+
244
+ Every call to the Halo SDK must include a valid JWT. The SDK requests one via the `onRequestJWT` callback whenever it needs to authenticate.
245
+
246
+ A JWT must be obtained from your backend or generated using your RSA private key and the credentials from the [developer portal](https://go.developerportal.qa.haloplus.io/). See the [Halo developer documentation](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk) for the required claims. For testing, paste a valid token directly into `Config.tempJwt` as shown below.
247
+
248
+ Split your config and JWT supply into two files:
249
+
250
+ **`src/config.ts`** — stores your app settings (never commit real values to source control):
251
+
252
+ ```typescript
253
+ export const Config = {
254
+ // Paste a valid JWT here for testing.
255
+ // Regenerate it if the SDK reports JWTExpired on startup.
256
+ tempJwt: 'eyJ...',
257
+
258
+ // Obtained from the developer portal
259
+ applicationPackageName: 'com.yourcompany.myapp',
260
+ applicationVersion: '1.0.0',
261
+ onStartTransactionTimeOut: 300000, // ms before "tap card" times out
262
+ enableSchemeAnimations: true, // show Visa/Mastercard animations
263
+ } as const;
264
+ ```
265
+
266
+ **`src/jwt/JwtToken.ts`**: supplies the JWT when the SDK asks for one:
267
+
268
+ ```typescript
269
+ import { Config } from '../config';
270
+
271
+ export function getJwt(): string {
272
+ if (Config.tempJwt) {
273
+ return Config.tempJwt;
274
+ }
275
+ throw new Error('No JWT configured. Set Config.tempJwt in src/config.ts.');
276
+ }
277
+ ```
278
+
279
+ > **Important:** Add `config.ts` to `.gitignore` so you never accidentally commit a real token to source control.
280
+
281
+ ---
63
282
 
64
283
  ## Usage
65
284
 
285
+ ### Step 1 — Request Permissions
286
+
287
+ Request the Android runtime permissions before initialising the SDK. Android 12+ (API 31+) uses new Bluetooth permission names.
288
+
66
289
  ```typescript
290
+ // src/permissions.ts
291
+ import { PermissionsAndroid, Platform } from 'react-native';
292
+
293
+ export async function requestHaloPermissions(): Promise<void> {
294
+ if (Platform.OS !== 'android') return;
295
+
296
+ const sdkVersion =
297
+ typeof Platform.Version === 'number'
298
+ ? Platform.Version
299
+ : parseInt(Platform.Version, 10);
300
+
301
+ const permissions: string[] = [
302
+ PermissionsAndroid.PERMISSIONS.CAMERA,
303
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
304
+ ];
305
+
306
+ if (sdkVersion >= 31) {
307
+ // Android 12+ Bluetooth permissions
308
+ permissions.push(
309
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
310
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
311
+ );
312
+ }
313
+
314
+ await PermissionsAndroid.requestMultiple(permissions);
315
+ }
316
+ ```
317
+
318
+ ### Step 2 — Set Up Callbacks
319
+
320
+ The SDK communicates back to your app through an `IHaloCallbacks` object you provide. Each callback corresponds to a different type of event.
321
+
322
+ ```typescript
323
+ // src/haloCallbacks.ts
67
324
  import {
68
- HaloSdk,
69
325
  type IHaloCallbacks,
326
+ type HaloAttestationHealthResult,
70
327
  type HaloInitializationResult,
71
328
  type HaloTransactionResult,
72
329
  type HaloUIMessage,
73
- type HaloAttestationHealthResult,
74
330
  } from 'halo-sdk-react-native';
331
+ import { getJwt } from './jwt/JwtToken';
332
+
333
+ export function buildCallbacks(options: {
334
+ onStatusChange: (msg: string) => void;
335
+ onError: (msg: string) => void;
336
+ onTransactionResult: (result: HaloTransactionResult) => void;
337
+ }): IHaloCallbacks {
338
+ return {
339
+ // Called when the SDK finishes starting up.
340
+ // Check resultType === 'Initialized' for successful init.
341
+ onInitializationResult(result: HaloInitializationResult) {
342
+ if (result.resultType === 'Initialized') {
343
+ options.onStatusChange('SDK ready — present a card to pay');
344
+ } else {
345
+ options.onError(`Initialisation failed: ${result.resultType} (${result.errorCode})`);
346
+ }
347
+ },
348
+
349
+ // Called when a card tap completes (approved, declined, or error)
350
+ onHaloTransactionResult(result: HaloTransactionResult) {
351
+ options.onTransactionResult(result);
352
+ },
353
+
354
+ // Called repeatedly during a transaction to tell you what to show the user
355
+ // e.g. "PRESENT_CARD", "PROCESSING", "APPROVED"
356
+ onHaloUIMessage(message: HaloUIMessage) {
357
+ options.onStatusChange(message.msgID);
358
+ },
359
+
360
+ // The SDK asks you for a fresh JWT whenever it needs one
361
+ onRequestJWT(jwtCallback: (jwt: string) => void) {
362
+ try {
363
+ jwtCallback(getJwt());
364
+ } catch (err: any) {
365
+ options.onError(`JWT error: ${err.message}`);
366
+ }
367
+ },
368
+
369
+ // Device failed attestation (tampered OS, emulator, etc.)
370
+ onAttestationError(details: HaloAttestationHealthResult) {
371
+ options.onError(`Attestation error: ${details.errorCode}`);
372
+ },
373
+
374
+ // A security check failed (e.g. invalid JWT, revoked merchant)
375
+ onSecurityError(errorCode: string) {
376
+ options.onError(`Security error: ${errorCode}`);
377
+ },
378
+
379
+ // The SDK released the camera (e.g. card scheme animation finished)
380
+ onCameraControlLost() {
381
+ console.log('Camera released by SDK');
382
+ },
383
+ };
384
+ }
385
+ ```
386
+
387
+ ### Step 3 — Initialize the SDK
388
+
389
+ Call `HaloSdk.initialize` once, before running any transactions. A good place is in a `useEffect` when your payment screen mounts.
390
+
391
+ ```typescript
392
+ import { HaloSdk } from 'halo-sdk-react-native';
393
+ import { Config } from './config';
394
+ import { requestHaloPermissions } from './permissions';
395
+ import { buildCallbacks } from './haloCallbacks';
396
+
397
+ async function setupSdk(
398
+ onStatusChange: (msg: string) => void,
399
+ onError: (msg: string) => void,
400
+ onTransactionResult: (result: any) => void,
401
+ ): Promise<void> {
402
+ // 1. Request permissions first
403
+ await requestHaloPermissions();
404
+
405
+ // 2. Build your callbacks
406
+ const callbacks = buildCallbacks({ onStatusChange, onError, onTransactionResult });
407
+
408
+ // 3. Initialise — the SDK will call onInitializationResult when ready
409
+ await HaloSdk.initialize(
410
+ callbacks,
411
+ Config.applicationPackageName, // e.g. 'com.yourcompany.myapp'
412
+ Config.applicationVersion, // e.g. '1.0.0'
413
+ Config.onStartTransactionTimeOut, // ms before a tap times out (default 300000)
414
+ Config.enableSchemeAnimations, // show Visa/Mastercard animations
415
+ );
416
+ }
417
+ ```
418
+
419
+ ### Step 4 — Run a Transaction
75
420
 
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
421
+ Once the SDK is initialised, you can charge a card:
422
+
423
+ ```typescript
424
+ // Charge R 10.50
425
+ const result = await HaloSdk.startTransaction(10.50, 'ORDER-001', 'ZAR');
426
+ console.log(result.resultType); // e.g. "tap_started"
427
+
428
+ // Refund R 10.50 to the original card
429
+ const refund = await HaloSdk.cardRefundTransaction(10.50, 'ORDER-001', 'ZAR');
430
+
431
+ // Cancel a transaction that is waiting for a tap
117
432
  await HaloSdk.cancelTransaction();
118
433
  ```
119
434
 
120
- ## API
435
+ `startTransaction` and `cardRefundTransaction` resolve immediately once the tap is registered, the final payment outcome arrives through `onHaloTransactionResult`.
436
+
437
+ ### Full Example
438
+
439
+ Below is a minimal but complete payment screen taken directly from the example app in this repo:
440
+
441
+ ```tsx
442
+ // App.tsx
443
+ import React, { useEffect, useState } from 'react';
444
+ import {
445
+ ActivityIndicator,
446
+ Text,
447
+ TextInput,
448
+ TouchableOpacity,
449
+ View,
450
+ } from 'react-native';
451
+ import {
452
+ HaloSdk,
453
+ type HaloTransactionResult,
454
+ type IHaloCallbacks,
455
+ } from 'halo-sdk-react-native';
456
+ import { getJwt } from './src/jwt/JwtToken';
457
+ import { Config } from './src/config';
458
+ import { requestHaloPermissions } from './src/permissions';
459
+
460
+ export default function App() {
461
+ const [amount, setAmount] = useState('');
462
+ const [merchantRef, setMerchantRef] = useState('');
463
+ const [status, setStatus] = useState('Initialising...');
464
+ const [isReady, setIsReady] = useState(false);
465
+
466
+ useEffect(() => {
467
+ initSdk();
468
+ }, []);
469
+
470
+ async function initSdk() {
471
+ await requestHaloPermissions();
472
+
473
+ const callbacks: IHaloCallbacks = {
474
+ onInitializationResult(result) {
475
+ setIsReady(result.resultType === 'Initialized');
476
+ setStatus(
477
+ result.resultType === 'Initialized'
478
+ ? 'Ready — enter amount and tap Charge'
479
+ : `Init failed: ${result.resultType} (${result.errorCode})`,
480
+ );
481
+ },
482
+ onHaloTransactionResult(result: HaloTransactionResult) {
483
+ setStatus(`Result: ${result.resultType} ${result.errorCode}`);
484
+ },
485
+ onHaloUIMessage(message) {
486
+ // The SDK sends messages like "PRESENT_CARD", "PROCESSING", "APPROVED"
487
+ setStatus(message.msgID);
488
+ },
489
+ onRequestJWT(jwtCallback) {
490
+ try {
491
+ jwtCallback(getJwt());
492
+ } catch (e: any) {
493
+ setStatus(`JWT error: ${e.message}`);
494
+ }
495
+ },
496
+ onAttestationError(details) {
497
+ setStatus(`Attestation error: ${details.errorCode}`);
498
+ },
499
+ onSecurityError(errorCode) {
500
+ setStatus(`Security error: ${errorCode}`);
501
+ },
502
+ onCameraControlLost() {},
503
+ };
504
+
505
+ HaloSdk.initialize(
506
+ callbacks,
507
+ Config.applicationPackageName,
508
+ Config.applicationVersion,
509
+ Config.onStartTransactionTimeOut,
510
+ Config.enableSchemeAnimations,
511
+ ).catch(e => setStatus(`SDK error: ${e.message}`));
512
+ }
513
+
514
+ async function charge() {
515
+ if (!amount || !merchantRef) {
516
+ setStatus('Please enter amount and merchant reference');
517
+ return;
518
+ }
519
+ try {
520
+ const result = await HaloSdk.startTransaction(parseFloat(amount), merchantRef, 'ZAR');
521
+ setStatus(`Tap accepted: ${result.resultType}`);
522
+ } catch (e: any) {
523
+ setStatus(`Error: ${e.message}`);
524
+ }
525
+ }
526
+
527
+ return (
528
+ <View style={{ flex: 1, padding: 24, justifyContent: 'center' }}>
529
+ <Text style={{ fontSize: 13, color: '#555', marginBottom: 16 }}>{status}</Text>
530
+
531
+ {!isReady ? (
532
+ <ActivityIndicator size="large" />
533
+ ) : (
534
+ <>
535
+ <TextInput
536
+ placeholder="Amount (e.g. 10.50)"
537
+ value={amount}
538
+ onChangeText={setAmount}
539
+ keyboardType="decimal-pad"
540
+ style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 8, borderRadius: 4 }}
541
+ />
542
+ <TextInput
543
+ placeholder="Merchant reference (e.g. ORDER-001)"
544
+ value={merchantRef}
545
+ onChangeText={setMerchantRef}
546
+ style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 16, borderRadius: 4 }}
547
+ />
548
+ <TouchableOpacity
549
+ onPress={charge}
550
+ style={{ backgroundColor: '#1976D2', padding: 14, borderRadius: 8, alignItems: 'center' }}>
551
+ <Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>Charge</Text>
552
+ </TouchableOpacity>
553
+ </>
554
+ )}
555
+ </View>
556
+ );
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## API Reference
121
563
 
122
564
  ### `HaloSdk.initialize(callbacks, packageName, version, timeout?, animations?)`
123
565
 
124
566
  Initialises the SDK. Must be called before any transaction methods.
125
567
 
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 |
568
+ | Parameter | Type | Default | Description |
569
+ |---|---|---|---|
570
+ | `callbacks` | `IHaloCallbacks` | | Your callback handler object |
571
+ | `applicationPackageName` | `string` | — | Your app's Android package name |
572
+ | `applicationVersion` | `string` | — | Your app's version string |
573
+ | `onStartTransactionTimeOut` | `number?` | `300000` | Time in ms to wait for a card tap |
574
+ | `enableSchemeAnimations` | `boolean?` | `false` | Show Visa/Mastercard/Amex animations on approval |
133
575
 
134
576
  ### `HaloSdk.startTransaction(amount, reference, currency)`
135
577
 
136
- Starts a purchase transaction. Resolves with a `HaloStartTransactionResult` once the card tap is accepted or rejected.
578
+ Starts a purchase transaction. Prompts the user to tap their card.
579
+
580
+ | Parameter | Type | Example |
581
+ |---|---|---|
582
+ | `transactionAmount` | `number` | `10.50` |
583
+ | `merchantTransactionReference` | `string` | `'ORDER-001'` |
584
+ | `transactionCurrency` | `string` | `'ZAR'` |
585
+
586
+ Returns `Promise<HaloStartTransactionResult>` — resolves when the card tap is registered. The final outcome arrives via `onHaloTransactionResult`.
137
587
 
138
588
  ### `HaloSdk.cardRefundTransaction(amount, reference, currency)`
139
589
 
140
- Starts a card-present refund transaction.
590
+ Starts a card-present refund. Same parameters as `startTransaction`.
141
591
 
142
592
  ### `HaloSdk.cancelTransaction()`
143
593
 
144
- Requests cancellation of the current in-progress transaction.
594
+ Cancels the current in-progress transaction (e.g. if the user presses Cancel while waiting for a tap).
145
595
 
146
- ## Callbacks (`IHaloCallbacks`)
596
+ ### Callbacks (`IHaloCallbacks`)
147
597
 
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 |
598
+ | Callback | When it fires |
599
+ |---|---|
600
+ | `onInitializationResult(result)` | SDK startup complete (success or failure) |
601
+ | `onHaloTransactionResult(result)` | Final payment outcome; approved, declined, or error |
602
+ | `onHaloUIMessage(message)` | Prompt to show the user during a tap: `PRESENT_CARD`, `PROCESSING`, `APPROVED`, etc. |
603
+ | `onRequestJWT(jwtCallback)` | SDK needs a fresh JWT; call `jwtCallback(yourJwtString)` |
604
+ | `onAttestationError(details)` | Device integrity check failed (rooted device, emulator, etc.) |
605
+ | `onSecurityError(errorCode)` | JWT invalid, merchant revoked, or other security failure |
606
+ | `onCameraControlLost()` | SDK has finished using the camera |
157
607
 
158
- ## Building the plugin
608
+ ### Result Types
159
609
 
160
- ```bash
161
- npm install
162
- npm run build
610
+ `resultType` and `errorCode` are plain strings serialised from native Android enums. Use these known values in your conditional logic.
611
+
612
+ **`HaloInitializationResult.resultType`**
613
+
614
+ | Value | Meaning |
615
+ |---|---|
616
+ | `'Initialized'` | SDK ready; safe to call `startTransaction` |
617
+ | `'RemoteAttestationFailure'` | Cloud attestation failed; inspect `errorCode` for the reason |
618
+ | `'LocalAttestationFailure'` | Device integrity check failed (rooted device, emulator, etc.) |
619
+
620
+ **`HaloInitializationResult.errorCode`** (when `resultType !== 'Initialized'`)
621
+
622
+ | Value | Meaning |
623
+ |---|---|
624
+ | `'OK'` | No error — accompanies `resultType: 'Initialized'` |
625
+ | `'JWTExpired'` | The JWT has expired — generate a fresh one |
626
+
627
+
628
+ **`HaloTransactionResult.resultType`**
629
+
630
+ | Value | Meaning |
631
+ |---|---|
632
+ | `'Approved'` | Transaction approved by the payment network |
633
+ | `'Declined'` | Card declined |
634
+ | `'Cancelled'` | Transaction cancelled (e.g. via `cancelTransaction()`) |
635
+ | `'Error'` | An error occurred — inspect `errorCode` and `errorDetails` |
636
+
637
+ ---
638
+
639
+ ## Documentation
640
+
641
+ Full SDK documentation: [halo-dot-developer-docs.gitbook.io](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk)
642
+
643
+ ---
644
+
645
+ ## Testing
646
+
647
+ All transactions are void until your NDA is fully executed.
648
+
649
+ 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.
650
+
651
+ ---
652
+
653
+ ## FAQ
654
+
655
+ **Q: How do I set `compileSdkVersion` / `minSdkVersion` if they're not set or causing issues?**
656
+
657
+ You can define them in `android/local.properties` and read them in Gradle:
658
+
659
+ ```properties
660
+ sdk.dir=C\:\\Users\\yourname\\Library\\Android/Sdk
661
+ aws.accesskey=YOUR_ACCESS_KEY
662
+ aws.secretkey=YOUR_SECRET_KEY
663
+ compileSdkVersion=34
664
+ minSdkVersion=29
665
+ ```
666
+
667
+ ```gradle
668
+ // android/app/build.gradle
669
+ compileSdkVersion localProperties.getProperty('compileSdkVersion').toInteger()
163
670
  ```
671
+
672
+ ---
673
+
674
+ **Q: The SDK fails to download / Gradle sync fails.**
675
+
676
+ - Confirm `aws.accesskey` and `aws.secretkey` are in `android/local.properties` with the exact casing shown
677
+ - Open the `android` folder in Android Studio and run **File → Sync Project with Gradle Files**
678
+ - Make sure you accepted the NDA on the developer portal (access is blocked until the NDA is signed)
679
+
680
+ ---
681
+
682
+ **Q: I get a build error about `HaloReactActivity` or `HaloSdkPackage` not found.**
683
+
684
+ - Confirm the npm package is installed: `npm install halo-sdk-react-native`
685
+ - Confirm `HaloSdkPackage()` is added in `MainApplication.kt`
686
+ - Confirm `MainActivity` extends `HaloReactActivity` (not `ReactActivity`)
687
+ - Run a Gradle sync in Android Studio
688
+
689
+ ---
690
+
691
+ **Q: The SDK initialises but transactions never complete.**
692
+
693
+ - Check that all [AndroidManifest permissions](#androidmanifest-permissions) are declared, especially the NFC `intent-filter` block
694
+ - Check that the NFC intent-filter is inside the `<activity>` block for `MainActivity`
695
+ - Verify your JWT is valid using `POST https://kernelserver.qa.haloplus.io/<sdk-version>/tokens/checkjwt`
696
+ - Check that `minSdkVersion` ≥ 29 and `compileSdkVersion` / `targetSdkVersion` ≥ 34
697
+
698
+ ---
699
+
700
+ **Q: How do I get a JWT for testing?**
701
+
702
+ A JWT must be generated using your RSA private key and the credentials from the [developer portal](https://go.developerportal.qa.haloplus.io/). See the [Halo developer documentation](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk) for the required claims and signing algorithm. Once you have a valid token, paste it into `Config.tempJwt` in your `config.ts`.
703
+
704
+ ---
705
+
706
+ **Q: `onInitializationResult` fires with `resultType: 'RemoteAttestationFailure'` and `errorCode: 'JWTExpired'`.**
707
+
708
+ Your temp JWT has expired. Developer-portal tokens are short-lived (typically 15 minutes). This is expected behaviour — the SDK fires a failure callback with the cached/expired token, then automatically requests a new JWT via `onRequestJWT` and retries. If `onInitializationResult` fires a **second time** shortly after with `resultType: 'Initialized'`, everything is working correctly.
709
+
710
+ If the second successful callback never arrives:
711
+ 1. Generate a fresh token and update `Config.tempJwt`
712
+ 2. Make sure your `onRequestJWT` callback calls `jwtCallback(yourJwt)` synchronously — async calls are not supported without extra handling
@@ -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 ->
@@ -81,8 +81,8 @@ dependencies {
81
81
  implementation 'androidx.appcompat:appcompat:1.6.1'
82
82
 
83
83
  // Halo SDK
84
- releaseImplementation group: "za.co.synthesis.halo", name: "sdk", version: "$sdkVersion", changing: true
85
- debugImplementation group: "za.co.synthesis.halo", name: "sdk", version: "$sdkVersion-debug", changing: true
84
+ releaseApi group: "za.co.synthesis.halo", name: "sdk", version: "$sdkVersion", changing: true
85
+ debugApi group: "za.co.synthesis.halo", name: "sdk", version: "$sdkVersion-debug", changing: true
86
86
 
87
87
  implementation(name: 'VisaSensoryBranding', ext: 'aar')
88
88
  implementation(name: 'sonic-sdk-release-1.5.0', ext: 'aar')
@@ -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.2",
4
4
  "description": "React Native plugin for Halo SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,8 @@
10
10
  "android/libs",
11
11
  "android/build.gradle",
12
12
  "android/settings.gradle",
13
- "README.md"
13
+ "README.md",
14
+ "CHANGELOG.md"
14
15
  ],
15
16
  "scripts": {
16
17
  "build": "tsc"