halo-sdk-react-native 1.0.4 → 1.0.5

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,735 +1,735 @@
1
- # halo-sdk-react-native
2
-
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
- ---
35
-
36
- ## Requirements
37
-
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 |
46
-
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))
51
-
52
- > **Note:** Android is the only supported platform for now.
53
-
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:
77
-
78
- ```bash
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
- }
99
- ```
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
-
107
- ```bash
108
- npm install halo-sdk-react-native
109
- # or
110
- yarn add halo-sdk-react-native
111
- ```
112
-
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):
114
-
115
- ```properties
116
- aws.accesskey=YOUR_ACCESS_KEY
117
- aws.secretkey=YOUR_SECRET_KEY
118
- ```
119
-
120
- > **Important:** Never commit `local.properties` to source control. Add it to your `.gitignore`
121
-
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):
123
-
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
- }
132
- ```
133
-
134
- **4.** Add the following `packagingOptions` block inside the `android { }` closure in `android/app/build.gradle`. This prevents a duplicate-file error caused by OSGI metadata bundled in the SDK's transitive dependencies:
135
-
136
- ```gradle
137
- android {
138
- // ... your existing config ...
139
-
140
- packagingOptions {
141
- resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
142
- }
143
- }
144
- ```
145
-
146
- ### Native Module Setup
147
-
148
- **5.** Open `android/app/src/main/kotlin/.../MainActivity.kt` and extend `HaloReactActivity` instead of `ReactActivity`:
149
-
150
- ```kotlin
151
- import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
152
- import com.facebook.react.defaults.DefaultReactActivityDelegate
153
- import za.co.synthesis.halo.sdkreactnativeplugin.HaloReactActivity
154
-
155
- class MainActivity : HaloReactActivity() {
156
-
157
- override fun getMainComponentName(): String = "MyHaloApp"
158
-
159
- override fun createReactActivityDelegate() =
160
- DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
161
- }
162
- ```
163
-
164
- This replaces `ReactActivity` so that NFC foreground dispatch and the Halo SDK lifecycle are managed automatically.
165
-
166
- ### AndroidManifest Permissions
167
-
168
- **6.** Add the required permissions to `android/app/src/main/AndroidManifest.xml`:
169
-
170
- ```xml
171
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
172
- xmlns:tools="http://schemas.android.com/tools">
173
-
174
- <uses-feature
175
- android:name="android.hardware.camera"
176
- android:required="false" />
177
-
178
- <!-- Bluetooth — legacy permissions for API 29/30 -->
179
- <uses-permission android:name="android.permission.BLUETOOTH" />
180
- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
181
-
182
- <!-- Bluetooth — API 31+ -->
183
- <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
184
- android:usesPermissionFlags="neverForLocation" />
185
- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
186
-
187
- <!-- Location (required for Bluetooth LE scanning) -->
188
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
189
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
190
- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
191
-
192
- <!-- Other -->
193
- <uses-permission android:name="android.permission.INTERNET" />
194
- <uses-permission android:name="android.permission.CAMERA" />
195
- <uses-permission android:name="android.permission.NFC" />
196
-
197
- <uses-feature android:name="android.hardware.nfc" android:required="true" />
198
-
199
- <!--
200
- tools:replace is required because the Halo SDK (and its bundled Visa library)
201
- declare android:label and android:allowBackup in their own manifests.
202
- Without these overrides the manifest merger will refuse to build.
203
- -->
204
- <application
205
- ...
206
- tools:replace="android:label,android:allowBackup">
207
- <activity
208
- android:name=".MainActivity"
209
- ...>
210
- <intent-filter>
211
- <action android:name="android.intent.action.MAIN" />
212
- <category android:name="android.intent.category.LAUNCHER" />
213
- </intent-filter>
214
-
215
- <!-- Required: NFC foreground dispatch -->
216
- <intent-filter>
217
- <action android:name="android.nfc.action.TECH_DISCOVERED" />
218
- </intent-filter>
219
- </activity>
220
- </application>
221
-
222
- </manifest>
223
- ```
224
-
225
- ---
226
-
227
- ## JWT — What It Is and How to Set It Up
228
-
229
- Every call to the Halo SDK must include a valid JWT. The SDK requests one via the `onRequestJWT` callback whenever it needs to authenticate.
230
-
231
- 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.
232
-
233
- Split your config and JWT supply into two files:
234
-
235
- **`src/config.ts`** — stores your app settings (never commit real values to source control):
236
-
237
- ```typescript
238
- export const Config = {
239
- // Paste a valid JWT here for testing.
240
- // Regenerate it if the SDK reports JWTExpired on startup.
241
- tempJwt: 'eyJ...',
242
-
243
- // Obtained from the developer portal
244
- applicationPackageName: 'com.yourcompany.myapp',
245
- applicationVersion: '1.0.0',
246
- onStartTransactionTimeOut: 300000, // ms before "tap card" times out
247
- enableSchemeAnimations: true, // show Visa/Mastercard animations
248
- } as const;
249
- ```
250
-
251
- **`src/jwt/JwtToken.ts`**: supplies the JWT when the SDK asks for one:
252
-
253
- ```typescript
254
- import { Config } from '../config';
255
-
256
- export function getJwt(): string {
257
- if (Config.tempJwt) {
258
- return Config.tempJwt;
259
- }
260
- throw new Error('No JWT configured. Set Config.tempJwt in src/config.ts.');
261
- }
262
- ```
263
-
264
- > **Important:** Add `config.ts` to `.gitignore` so you never accidentally commit a real token to source control.
265
-
266
- ---
267
-
268
- ## Usage
269
-
270
- ### Step 1 — Request Permissions
271
-
272
- Request the Android runtime permissions before initialising the SDK. Android 12+ (API 31+) uses new Bluetooth permission names.
273
-
274
- ```typescript
275
- // src/permissions.ts
276
- import { PermissionsAndroid, Platform } from 'react-native';
277
-
278
- export async function requestHaloPermissions(): Promise<void> {
279
- if (Platform.OS !== 'android') return;
280
-
281
- const sdkVersion =
282
- typeof Platform.Version === 'number'
283
- ? Platform.Version
284
- : parseInt(Platform.Version, 10);
285
-
286
- const permissions: string[] = [
287
- PermissionsAndroid.PERMISSIONS.CAMERA,
288
- PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
289
- ];
290
-
291
- if (sdkVersion >= 31) {
292
- // Android 12+ Bluetooth permissions
293
- permissions.push(
294
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
295
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
296
- );
297
- }
298
-
299
- await PermissionsAndroid.requestMultiple(permissions);
300
- }
301
- ```
302
-
303
- ### Step 2 — Set Up Callbacks
304
-
305
- The SDK communicates back to your app through an `IHaloCallbacks` object you provide. Each callback corresponds to a different type of event.
306
-
307
- ```typescript
308
- // src/haloCallbacks.ts
309
- import {
310
- type IHaloCallbacks,
311
- type HaloAttestationHealthResult,
312
- type HaloInitializationResult,
313
- type HaloTransactionResult,
314
- type HaloUIMessage,
315
- } from 'halo-sdk-react-native';
316
- import { getJwt } from './jwt/JwtToken';
317
-
318
- export function buildCallbacks(options: {
319
- onStatusChange: (msg: string) => void;
320
- onError: (msg: string) => void;
321
- onTransactionResult: (result: HaloTransactionResult) => void;
322
- }): IHaloCallbacks {
323
- return {
324
- // Called when the SDK finishes starting up.
325
- // Check resultType === 'Initialized' for successful init.
326
- onInitializationResult(result: HaloInitializationResult) {
327
- if (result.resultType === 'Initialized') {
328
- options.onStatusChange('SDK ready — present a card to pay');
329
- } else {
330
- options.onError(`Initialisation failed: ${result.resultType} (${result.errorCode})`);
331
- }
332
- },
333
-
334
- // Called when a card tap completes (approved, declined, or error)
335
- onHaloTransactionResult(result: HaloTransactionResult) {
336
- options.onTransactionResult(result);
337
- },
338
-
339
- // Called repeatedly during a transaction to tell you what to show the user
340
- // e.g. "PRESENT_CARD", "PROCESSING", "APPROVED"
341
- onHaloUIMessage(message: HaloUIMessage) {
342
- options.onStatusChange(message.msgID);
343
- },
344
-
345
- // The SDK asks you for a fresh JWT whenever it needs one
346
- onRequestJWT(jwtCallback: (jwt: string) => void) {
347
- try {
348
- jwtCallback(getJwt());
349
- } catch (err: any) {
350
- options.onError(`JWT error: ${err.message}`);
351
- }
352
- },
353
-
354
- // Device failed attestation (tampered OS, emulator, etc.)
355
- onAttestationError(details: HaloAttestationHealthResult) {
356
- options.onError(`Attestation error: ${details.errorCode}`);
357
- },
358
-
359
- // A security check failed (e.g. invalid JWT, revoked merchant)
360
- onSecurityError(errorCode: string) {
361
- options.onError(`Security error: ${errorCode}`);
362
- },
363
-
364
- // The SDK released the camera (e.g. card scheme animation finished)
365
- onCameraControlLost() {
366
- console.log('Camera released by SDK');
367
- },
368
- };
369
- }
370
- ```
371
-
372
- ### Step 3 — Initialize the SDK
373
-
374
- Call `HaloSdk.initialize` once, before running any transactions. A good place is in a `useEffect` when your payment screen mounts.
375
-
376
- ```typescript
377
- import { HaloSdk } from 'halo-sdk-react-native';
378
- import { Config } from './config';
379
- import { requestHaloPermissions } from './permissions';
380
- import { buildCallbacks } from './haloCallbacks';
381
-
382
- async function setupSdk(
383
- onStatusChange: (msg: string) => void,
384
- onError: (msg: string) => void,
385
- onTransactionResult: (result: any) => void,
386
- ): Promise<void> {
387
- // 1. Request permissions first
388
- await requestHaloPermissions();
389
-
390
- // 2. Build your callbacks
391
- const callbacks = buildCallbacks({ onStatusChange, onError, onTransactionResult });
392
-
393
- // 3. Initialise — the SDK will call onInitializationResult when ready
394
- await HaloSdk.initialize(
395
- callbacks,
396
- Config.applicationPackageName, // e.g. 'com.yourcompany.myapp'
397
- Config.applicationVersion, // e.g. '1.0.0'
398
- Config.onStartTransactionTimeOut, // ms before a tap times out (default 300000)
399
- Config.enableSchemeAnimations, // show Visa/Mastercard animations
400
- );
401
- }
402
- ```
403
-
404
- ### Step 4 — Run a Transaction
405
-
406
- Once the SDK is initialised, you can charge a card:
407
-
408
- ```typescript
409
- // Charge R 10.50
410
- const result = await HaloSdk.startTransaction(10.50, 'ORDER-001', 'ZAR');
411
- console.log(result.resultType); // e.g. "tap_started"
412
-
413
- // Refund R 10.50 to the original card
414
- const refund = await HaloSdk.cardRefundTransaction(10.50, 'ORDER-001', 'ZAR');
415
-
416
- // Cancel a transaction that is waiting for a tap
417
- await HaloSdk.cancelTransaction();
418
- ```
419
-
420
- `startTransaction` and `cardRefundTransaction` resolve immediately once the tap is registered, the final payment outcome arrives through `onHaloTransactionResult`.
421
-
422
- ### Full Example
423
-
424
- Below is a minimal but complete payment screen taken directly from the example app in this repo:
425
-
426
- ```tsx
427
- // App.tsx
428
- import { useEffect, useState } from 'react';
429
- import {
430
- ActivityIndicator,
431
- Text,
432
- TextInput,
433
- TouchableOpacity,
434
- View,
435
- } from 'react-native';
436
- import {
437
- HaloSdk,
438
- type HaloTransactionResult,
439
- type IHaloCallbacks,
440
- } from 'halo-sdk-react-native';
441
- import { getJwt } from './src/jwt/JwtToken';
442
- import { Config } from './src/config';
443
- import { requestHaloPermissions } from './src/permissions';
444
-
445
- export default function App() {
446
- const [amount, setAmount] = useState('');
447
- const [merchantRef, setMerchantRef] = useState('');
448
- const [status, setStatus] = useState('Initialising...');
449
- const [isReady, setIsReady] = useState(false);
450
-
451
- useEffect(() => {
452
- initSdk();
453
- }, []);
454
-
455
- async function initSdk() {
456
- await requestHaloPermissions();
457
-
458
- const callbacks: IHaloCallbacks = {
459
- onInitializationResult(result) {
460
- setIsReady(result.resultType === 'Initialized');
461
- setStatus(
462
- result.resultType === 'Initialized'
463
- ? 'Ready — enter amount and tap Charge'
464
- : `Init failed: ${result.resultType} (${result.errorCode})`,
465
- );
466
- },
467
- onHaloTransactionResult(result: HaloTransactionResult) {
468
- setStatus(`Result: ${result.resultType} ${result.errorCode}`);
469
- },
470
- onHaloUIMessage(message) {
471
- // The SDK sends messages like "PRESENT_CARD", "PROCESSING", "APPROVED"
472
- setStatus(message.msgID);
473
- },
474
- onRequestJWT(jwtCallback) {
475
- try {
476
- jwtCallback(getJwt());
477
- } catch (e: any) {
478
- setStatus(`JWT error: ${e.message}`);
479
- }
480
- },
481
- onAttestationError(details) {
482
- setStatus(`Attestation error: ${details.errorCode}`);
483
- },
484
- onSecurityError(errorCode) {
485
- setStatus(`Security error: ${errorCode}`);
486
- },
487
- onCameraControlLost() {},
488
- };
489
-
490
- HaloSdk.initialize(
491
- callbacks,
492
- Config.applicationPackageName,
493
- Config.applicationVersion,
494
- Config.onStartTransactionTimeOut,
495
- Config.enableSchemeAnimations,
496
- ).catch(e => setStatus(`SDK error: ${e.message}`));
497
- }
498
-
499
- async function charge() {
500
- if (!amount || !merchantRef) {
501
- setStatus('Please enter amount and merchant reference');
502
- return;
503
- }
504
- try {
505
- const result = await HaloSdk.startTransaction(parseFloat(amount), merchantRef, 'ZAR');
506
- setStatus(`Tap accepted: ${result.resultType}`);
507
- } catch (e: any) {
508
- setStatus(`Error: ${e.message}`);
509
- }
510
- }
511
-
512
- return (
513
- <View style={{ flex: 1, padding: 24, justifyContent: 'center' }}>
514
- <Text style={{ fontSize: 13, color: '#555', marginBottom: 16 }}>{status}</Text>
515
-
516
- {!isReady ? (
517
- <ActivityIndicator size="large" />
518
- ) : (
519
- <>
520
- <TextInput
521
- placeholder="Amount (e.g. 10.50)"
522
- value={amount}
523
- onChangeText={setAmount}
524
- keyboardType="decimal-pad"
525
- style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 8, borderRadius: 4 }}
526
- />
527
- <TextInput
528
- placeholder="Merchant reference (e.g. ORDER-001)"
529
- value={merchantRef}
530
- onChangeText={setMerchantRef}
531
- style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 16, borderRadius: 4 }}
532
- />
533
- <TouchableOpacity
534
- onPress={charge}
535
- style={{ backgroundColor: '#1976D2', padding: 14, borderRadius: 8, alignItems: 'center' }}>
536
- <Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>Charge</Text>
537
- </TouchableOpacity>
538
- </>
539
- )}
540
- </View>
541
- );
542
- }
543
- ```
544
-
545
- ---
546
-
547
- ## API Reference
548
-
549
- ### `HaloSdk.initialize(callbacks, packageName, version, timeout?, animations?)`
550
-
551
- Initialises the SDK. Must be called before any transaction methods.
552
-
553
- | Parameter | Type | Default | Description |
554
- |---|---|---|---|
555
- | `callbacks` | `IHaloCallbacks` | — | Your callback handler object |
556
- | `applicationPackageName` | `string` | — | Your app's Android package name |
557
- | `applicationVersion` | `string` | — | Your app's version string |
558
- | `onStartTransactionTimeOut` | `number?` | `300000` | Time in ms to wait for a card tap |
559
- | `enableSchemeAnimations` | `boolean?` | `false` | Show Visa/Mastercard/Amex animations on approval |
560
-
561
- ### `HaloSdk.startTransaction(amount, reference, currency)`
562
-
563
- Starts a purchase transaction. Prompts the user to tap their card.
564
-
565
- | Parameter | Type | Example |
566
- |---|---|---|
567
- | `transactionAmount` | `number` | `10.50` |
568
- | `merchantTransactionReference` | `string` | `'ORDER-001'` |
569
- | `transactionCurrency` | `string` | `'ZAR'` |
570
-
571
- Returns `Promise<HaloStartTransactionResult>` — resolves when the card tap is registered. The final outcome arrives via `onHaloTransactionResult`.
572
-
573
- ### `HaloSdk.cardRefundTransaction(amount, reference, currency)`
574
-
575
- Starts a card-present refund. Same parameters as `startTransaction`.
576
-
577
- ### `HaloSdk.cancelTransaction()`
578
-
579
- Cancels the current in-progress transaction (e.g. if the user presses Cancel while waiting for a tap).
580
-
581
- ### Callbacks (`IHaloCallbacks`)
582
-
583
- | Callback | When it fires |
584
- |---|---|
585
- | `onInitializationResult(result)` | SDK startup complete (success or failure) |
586
- | `onHaloTransactionResult(result)` | Final payment outcome; approved, declined, or error |
587
- | `onHaloUIMessage(message)` | Prompt to show the user during a tap: `PRESENT_CARD`, `PROCESSING`, `APPROVED`, etc. |
588
- | `onRequestJWT(jwtCallback)` | SDK needs a fresh JWT; call `jwtCallback(yourJwtString)` |
589
- | `onAttestationError(details)` | Device integrity check failed (rooted device, emulator, etc.) |
590
- | `onSecurityError(errorCode)` | JWT invalid, merchant revoked, or other security failure |
591
- | `onCameraControlLost()` | SDK has finished using the camera |
592
-
593
- ### Result Types
594
-
595
- `resultType` and `errorCode` are plain strings serialised from native Android enums. Use these known values in your conditional logic.
596
-
597
- **`HaloInitializationResult.resultType`**
598
-
599
- | Value | Meaning |
600
- |---|---|
601
- | `'Initialized'` | SDK ready; safe to call `startTransaction` |
602
- | `'RemoteAttestationFailure'` | Cloud attestation failed; inspect `errorCode` for the reason |
603
- | `'LocalAttestationFailure'` | Device integrity check failed (rooted device, emulator, etc.) |
604
-
605
- **`HaloInitializationResult.errorCode`** (when `resultType !== 'Initialized'`)
606
-
607
- | Value | Meaning |
608
- |---|---|
609
- | `'OK'` | No error — accompanies `resultType: 'Initialized'` |
610
- | `'JWTExpired'` | The JWT has expired — generate a fresh one |
611
-
612
-
613
- **`HaloTransactionResult.resultType`**
614
-
615
- | Value | Meaning |
616
- |---|---|
617
- | `'Approved'` | Transaction approved by the payment network |
618
- | `'Declined'` | Card declined |
619
- | `'Cancelled'` | Transaction cancelled (e.g. via `cancelTransaction()`) |
620
- | `'Error'` | An error occurred — inspect `errorCode` and `errorDetails` |
621
-
622
- ---
623
-
624
- ## Documentation
625
-
626
- Full SDK documentation: [halo-dot-developer-docs.gitbook.io](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk)
627
-
628
- ---
629
-
630
- ## Testing
631
-
632
- All transactions are void until your NDA is fully executed.
633
-
634
- 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.
635
-
636
- ---
637
-
638
- ## FAQ
639
-
640
- **Q: How do I set `compileSdkVersion` / `minSdkVersion` if they're not set or causing issues?**
641
-
642
- You can define them in `android/local.properties` and read them in Gradle:
643
-
644
- ```properties
645
- sdk.dir=C\:\\Users\\yourname\\Library\\Android/Sdk
646
- aws.accesskey=YOUR_ACCESS_KEY
647
- aws.secretkey=YOUR_SECRET_KEY
648
- compileSdkVersion=34
649
- minSdkVersion=29
650
- ```
651
-
652
- ```gradle
653
- // android/app/build.gradle
654
- compileSdkVersion localProperties.getProperty('compileSdkVersion').toInteger()
655
- ```
656
-
657
- ---
658
-
659
- **Q: The SDK fails to download / Gradle sync fails.**
660
-
661
- - Confirm `aws.accesskey` and `aws.secretkey` are in `android/local.properties` with the exact casing shown
662
- - Open the `android` folder in Android Studio and run **File → Sync Project with Gradle Files**
663
- - Make sure you accepted the NDA on the developer portal (access is blocked until the NDA is signed)
664
-
665
- ---
666
-
667
- **Q: I get a build error about `HaloReactActivity` or `HaloSdkPackage` not found.**
668
-
669
- - Confirm the npm package is installed: `npm install halo-sdk-react-native`
670
- - Confirm `MainActivity` extends `HaloReactActivity` (not `ReactActivity`)
671
- - Run a Gradle sync in Android Studio
672
-
673
- ---
674
-
675
- **Q: The SDK initialises but transactions never complete.**
676
-
677
- - Check that all [AndroidManifest permissions](#androidmanifest-permissions) are declared, especially the NFC `intent-filter` block
678
- - Check that the NFC intent-filter is inside the `<activity>` block for `MainActivity`
679
- - Verify your JWT is valid using `POST https://kernelserver.qa.haloplus.io/<sdk-version>/tokens/checkjwt`
680
- - Check that `minSdkVersion` ≥ 29 and `compileSdkVersion` / `targetSdkVersion` ≥ 34
681
-
682
- ---
683
-
684
- **Q: How do I get a JWT for testing?**
685
-
686
- 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`.
687
-
688
- ---
689
-
690
- **Q: Manifest merger fails with an attribute conflict (e.g. `android:label`, `android:allowBackup`).**
691
-
692
- The Halo SDK bundles several sub-libraries (Visa Sensory Branding, etc.), each with their own `AndroidManifest.xml`. Any `processDebugMainManifest` failure caused by an attribute clash is fixed by adding the conflicting attribute name to `tools:replace` on your `<application>` element:
693
-
694
- ```xml
695
- <application
696
- ...
697
- tools:replace="android:label,android:allowBackup">
698
- ```
699
-
700
- If you add a new attribute to `tools:replace` and the **same error persists on the very next build**, Gradle may have cached the previously failed manifest merge. Run a clean build and try again:
701
-
702
- ```bash
703
- cd android && ./gradlew clean
704
- ```
705
-
706
- Then re run your normal build (`npx react-native run-android` or Android Studio).
707
-
708
- ---
709
-
710
- **Q: TypeScript build errors about `customConditions` or `moduleResolution` after editing `tsconfig.json`.**
711
-
712
- Do not override `moduleResolution` in your project's `tsconfig.json`. The base `@react-native/typescript-config` sets `"moduleResolution": "bundler"`, which is the only value compatible with its `customConditions` setting. Overriding it to `"node"` causes a TypeScript error.
713
-
714
- Instead, extend the base config and only add project specific overrides:
715
-
716
- ```json
717
- {
718
- "extends": "@react-native/typescript-config/tsconfig.json",
719
- "compilerOptions": {
720
- "skipLibCheck": true
721
- }
722
- }
723
- ```
724
-
725
- `skipLibCheck: true` suppresses spurious type errors that originate inside `node_modules` (e.g. phantom `@types/react` v19 conflicts) without changing how your own code is compiled.
726
-
727
- ---
728
-
729
- **Q: `onInitializationResult` fires with `resultType: 'RemoteAttestationFailure'` and `errorCode: 'JWTExpired'`.**
730
-
731
- 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.
732
-
733
- If the second successful callback never arrives:
734
- 1. Generate a fresh token and update `Config.tempJwt`
735
- 2. Make sure your `onRequestJWT` callback calls `jwtCallback(yourJwt)` synchronously — async calls are not supported without extra handling
1
+ # halo-sdk-react-native
2
+
3
+ A React Native plugin 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
+ ---
35
+
36
+ ## Requirements
37
+
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 |
46
+
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))
51
+
52
+ > **Note:** Android is the only supported platform for now.
53
+
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:
77
+
78
+ ```bash
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
+ }
99
+ ```
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
+
107
+ ```bash
108
+ npm install halo-sdk-react-native
109
+ # or
110
+ yarn add halo-sdk-react-native
111
+ ```
112
+
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):
114
+
115
+ ```properties
116
+ aws.accesskey=YOUR_ACCESS_KEY
117
+ aws.secretkey=YOUR_SECRET_KEY
118
+ ```
119
+
120
+ > **Important:** Never commit `local.properties` to source control. Add it to your `.gitignore`
121
+
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):
123
+
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
+ }
132
+ ```
133
+
134
+ **4.** Add the following `packagingOptions` block inside the `android { }` closure in `android/app/build.gradle`. This prevents a duplicate-file error caused by OSGI metadata bundled in the SDK's transitive dependencies:
135
+
136
+ ```gradle
137
+ android {
138
+ // ... your existing config ...
139
+
140
+ packagingOptions {
141
+ resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Native Module Setup
147
+
148
+ **5.** Open `android/app/src/main/kotlin/.../MainActivity.kt` and extend `HaloReactActivity` instead of `ReactActivity`:
149
+
150
+ ```kotlin
151
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
152
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
153
+ import za.co.synthesis.halo.sdkreactnativeplugin.HaloReactActivity
154
+
155
+ class MainActivity : HaloReactActivity() {
156
+
157
+ override fun getMainComponentName(): String = "MyHaloApp"
158
+
159
+ override fun createReactActivityDelegate() =
160
+ DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
161
+ }
162
+ ```
163
+
164
+ This replaces `ReactActivity` so that NFC foreground dispatch and the Halo SDK lifecycle are managed automatically.
165
+
166
+ ### AndroidManifest Permissions
167
+
168
+ **6.** Add the required permissions to `android/app/src/main/AndroidManifest.xml`:
169
+
170
+ ```xml
171
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
172
+ xmlns:tools="http://schemas.android.com/tools">
173
+
174
+ <uses-feature
175
+ android:name="android.hardware.camera"
176
+ android:required="false" />
177
+
178
+ <!-- Bluetooth — legacy permissions for API 29/30 -->
179
+ <uses-permission android:name="android.permission.BLUETOOTH" />
180
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
181
+
182
+ <!-- Bluetooth — API 31+ -->
183
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
184
+ android:usesPermissionFlags="neverForLocation" />
185
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
186
+
187
+ <!-- Location (required for Bluetooth LE scanning) -->
188
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
189
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
190
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
191
+
192
+ <!-- Other -->
193
+ <uses-permission android:name="android.permission.INTERNET" />
194
+ <uses-permission android:name="android.permission.CAMERA" />
195
+ <uses-permission android:name="android.permission.NFC" />
196
+
197
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
198
+
199
+ <!--
200
+ tools:replace is required because the Halo SDK (and its bundled Visa library)
201
+ declare android:label and android:allowBackup in their own manifests.
202
+ Without these overrides the manifest merger will refuse to build.
203
+ -->
204
+ <application
205
+ ...
206
+ tools:replace="android:label,android:allowBackup">
207
+ <activity
208
+ android:name=".MainActivity"
209
+ ...>
210
+ <intent-filter>
211
+ <action android:name="android.intent.action.MAIN" />
212
+ <category android:name="android.intent.category.LAUNCHER" />
213
+ </intent-filter>
214
+
215
+ <!-- Required: NFC foreground dispatch -->
216
+ <intent-filter>
217
+ <action android:name="android.nfc.action.TECH_DISCOVERED" />
218
+ </intent-filter>
219
+ </activity>
220
+ </application>
221
+
222
+ </manifest>
223
+ ```
224
+
225
+ ---
226
+
227
+ ## JWT — What It Is and How to Set It Up
228
+
229
+ Every call to the Halo SDK must include a valid JWT. The SDK requests one via the `onRequestJWT` callback whenever it needs to authenticate.
230
+
231
+ 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.
232
+
233
+ Split your config and JWT supply into two files:
234
+
235
+ **`src/config.ts`** — stores your app settings (never commit real values to source control):
236
+
237
+ ```typescript
238
+ export const Config = {
239
+ // Paste a valid JWT here for testing.
240
+ // Regenerate it if the SDK reports JWTExpired on startup.
241
+ tempJwt: 'eyJ...',
242
+
243
+ // Obtained from the developer portal
244
+ applicationPackageName: 'com.yourcompany.myapp',
245
+ applicationVersion: '1.0.0',
246
+ onStartTransactionTimeOut: 300000, // ms before "tap card" times out
247
+ enableSchemeAnimations: true, // show Visa/Mastercard animations
248
+ } as const;
249
+ ```
250
+
251
+ **`src/jwt/JwtToken.ts`**: supplies the JWT when the SDK asks for one:
252
+
253
+ ```typescript
254
+ import { Config } from '../config';
255
+
256
+ export function getJwt(): string {
257
+ if (Config.tempJwt) {
258
+ return Config.tempJwt;
259
+ }
260
+ throw new Error('No JWT configured. Set Config.tempJwt in src/config.ts.');
261
+ }
262
+ ```
263
+
264
+ > **Important:** Add `config.ts` to `.gitignore` so you never accidentally commit a real token to source control.
265
+
266
+ ---
267
+
268
+ ## Usage
269
+
270
+ ### Step 1 — Request Permissions
271
+
272
+ Request the Android runtime permissions before initialising the SDK. Android 12+ (API 31+) uses new Bluetooth permission names.
273
+
274
+ ```typescript
275
+ // src/permissions.ts
276
+ import { PermissionsAndroid, Platform } from 'react-native';
277
+
278
+ export async function requestHaloPermissions(): Promise<void> {
279
+ if (Platform.OS !== 'android') return;
280
+
281
+ const sdkVersion =
282
+ typeof Platform.Version === 'number'
283
+ ? Platform.Version
284
+ : parseInt(Platform.Version, 10);
285
+
286
+ const permissions: string[] = [
287
+ PermissionsAndroid.PERMISSIONS.CAMERA,
288
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
289
+ ];
290
+
291
+ if (sdkVersion >= 31) {
292
+ // Android 12+ Bluetooth permissions
293
+ permissions.push(
294
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
295
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
296
+ );
297
+ }
298
+
299
+ await PermissionsAndroid.requestMultiple(permissions);
300
+ }
301
+ ```
302
+
303
+ ### Step 2 — Set Up Callbacks
304
+
305
+ The SDK communicates back to your app through an `IHaloCallbacks` object you provide. Each callback corresponds to a different type of event.
306
+
307
+ ```typescript
308
+ // src/haloCallbacks.ts
309
+ import {
310
+ type IHaloCallbacks,
311
+ type HaloAttestationHealthResult,
312
+ type HaloInitializationResult,
313
+ type HaloTransactionResult,
314
+ type HaloUIMessage,
315
+ } from 'halo-sdk-react-native';
316
+ import { getJwt } from './jwt/JwtToken';
317
+
318
+ export function buildCallbacks(options: {
319
+ onStatusChange: (msg: string) => void;
320
+ onError: (msg: string) => void;
321
+ onTransactionResult: (result: HaloTransactionResult) => void;
322
+ }): IHaloCallbacks {
323
+ return {
324
+ // Called when the SDK finishes starting up.
325
+ // Check resultType === 'Initialized' for successful init.
326
+ onInitializationResult(result: HaloInitializationResult) {
327
+ if (result.resultType === 'Initialized') {
328
+ options.onStatusChange('SDK ready — present a card to pay');
329
+ } else {
330
+ options.onError(`Initialisation failed: ${result.resultType} (${result.errorCode})`);
331
+ }
332
+ },
333
+
334
+ // Called when a card tap completes (approved, declined, or error)
335
+ onHaloTransactionResult(result: HaloTransactionResult) {
336
+ options.onTransactionResult(result);
337
+ },
338
+
339
+ // Called repeatedly during a transaction to tell you what to show the user
340
+ // e.g. "PRESENT_CARD", "PROCESSING", "APPROVED"
341
+ onHaloUIMessage(message: HaloUIMessage) {
342
+ options.onStatusChange(message.msgID);
343
+ },
344
+
345
+ // The SDK asks you for a fresh JWT whenever it needs one
346
+ onRequestJWT(jwtCallback: (jwt: string) => void) {
347
+ try {
348
+ jwtCallback(getJwt());
349
+ } catch (err: any) {
350
+ options.onError(`JWT error: ${err.message}`);
351
+ }
352
+ },
353
+
354
+ // Device failed attestation (tampered OS, emulator, etc.)
355
+ onAttestationError(details: HaloAttestationHealthResult) {
356
+ options.onError(`Attestation error: ${details.errorCode}`);
357
+ },
358
+
359
+ // A security check failed (e.g. invalid JWT, revoked merchant)
360
+ onSecurityError(errorCode: string) {
361
+ options.onError(`Security error: ${errorCode}`);
362
+ },
363
+
364
+ // The SDK released the camera (e.g. card scheme animation finished)
365
+ onCameraControlLost() {
366
+ console.log('Camera released by SDK');
367
+ },
368
+ };
369
+ }
370
+ ```
371
+
372
+ ### Step 3 — Initialize the SDK
373
+
374
+ Call `HaloSdk.initialize` once, before running any transactions. A good place is in a `useEffect` when your payment screen mounts.
375
+
376
+ ```typescript
377
+ import { HaloSdk } from 'halo-sdk-react-native';
378
+ import { Config } from './config';
379
+ import { requestHaloPermissions } from './permissions';
380
+ import { buildCallbacks } from './haloCallbacks';
381
+
382
+ async function setupSdk(
383
+ onStatusChange: (msg: string) => void,
384
+ onError: (msg: string) => void,
385
+ onTransactionResult: (result: any) => void,
386
+ ): Promise<void> {
387
+ // 1. Request permissions first
388
+ await requestHaloPermissions();
389
+
390
+ // 2. Build your callbacks
391
+ const callbacks = buildCallbacks({ onStatusChange, onError, onTransactionResult });
392
+
393
+ // 3. Initialise — the SDK will call onInitializationResult when ready
394
+ await HaloSdk.initialize(
395
+ callbacks,
396
+ Config.applicationPackageName, // e.g. 'com.yourcompany.myapp'
397
+ Config.applicationVersion, // e.g. '1.0.0'
398
+ Config.onStartTransactionTimeOut, // ms before a tap times out (default 300000)
399
+ Config.enableSchemeAnimations, // show Visa/Mastercard animations
400
+ );
401
+ }
402
+ ```
403
+
404
+ ### Step 4 — Run a Transaction
405
+
406
+ Once the SDK is initialised, you can charge a card:
407
+
408
+ ```typescript
409
+ // Charge R 10.50
410
+ const result = await HaloSdk.startTransaction(10.50, 'ORDER-001', 'ZAR');
411
+ console.log(result.resultType); // e.g. "tap_started"
412
+
413
+ // Refund R 10.50 to the original card
414
+ const refund = await HaloSdk.cardRefundTransaction(10.50, 'ORDER-001', 'ZAR');
415
+
416
+ // Cancel a transaction that is waiting for a tap
417
+ await HaloSdk.cancelTransaction();
418
+ ```
419
+
420
+ `startTransaction` and `cardRefundTransaction` resolve immediately once the tap is registered, the final payment outcome arrives through `onHaloTransactionResult`.
421
+
422
+ ### Full Example
423
+
424
+ Below is a minimal but complete payment screen taken directly from the example app in this repo:
425
+
426
+ ```tsx
427
+ // App.tsx
428
+ import { useEffect, useState } from 'react';
429
+ import {
430
+ ActivityIndicator,
431
+ Text,
432
+ TextInput,
433
+ TouchableOpacity,
434
+ View,
435
+ } from 'react-native';
436
+ import {
437
+ HaloSdk,
438
+ type HaloTransactionResult,
439
+ type IHaloCallbacks,
440
+ } from 'halo-sdk-react-native';
441
+ import { getJwt } from './src/jwt/JwtToken';
442
+ import { Config } from './src/config';
443
+ import { requestHaloPermissions } from './src/permissions';
444
+
445
+ export default function App() {
446
+ const [amount, setAmount] = useState('');
447
+ const [merchantRef, setMerchantRef] = useState('');
448
+ const [status, setStatus] = useState('Initialising...');
449
+ const [isReady, setIsReady] = useState(false);
450
+
451
+ useEffect(() => {
452
+ initSdk();
453
+ }, []);
454
+
455
+ async function initSdk() {
456
+ await requestHaloPermissions();
457
+
458
+ const callbacks: IHaloCallbacks = {
459
+ onInitializationResult(result) {
460
+ setIsReady(result.resultType === 'Initialized');
461
+ setStatus(
462
+ result.resultType === 'Initialized'
463
+ ? 'Ready — enter amount and tap Charge'
464
+ : `Init failed: ${result.resultType} (${result.errorCode})`,
465
+ );
466
+ },
467
+ onHaloTransactionResult(result: HaloTransactionResult) {
468
+ setStatus(`Result: ${result.resultType} ${result.errorCode}`);
469
+ },
470
+ onHaloUIMessage(message) {
471
+ // The SDK sends messages like "PRESENT_CARD", "PROCESSING", "APPROVED"
472
+ setStatus(message.msgID);
473
+ },
474
+ onRequestJWT(jwtCallback) {
475
+ try {
476
+ jwtCallback(getJwt());
477
+ } catch (e: any) {
478
+ setStatus(`JWT error: ${e.message}`);
479
+ }
480
+ },
481
+ onAttestationError(details) {
482
+ setStatus(`Attestation error: ${details.errorCode}`);
483
+ },
484
+ onSecurityError(errorCode) {
485
+ setStatus(`Security error: ${errorCode}`);
486
+ },
487
+ onCameraControlLost() {},
488
+ };
489
+
490
+ HaloSdk.initialize(
491
+ callbacks,
492
+ Config.applicationPackageName,
493
+ Config.applicationVersion,
494
+ Config.onStartTransactionTimeOut,
495
+ Config.enableSchemeAnimations,
496
+ ).catch(e => setStatus(`SDK error: ${e.message}`));
497
+ }
498
+
499
+ async function charge() {
500
+ if (!amount || !merchantRef) {
501
+ setStatus('Please enter amount and merchant reference');
502
+ return;
503
+ }
504
+ try {
505
+ const result = await HaloSdk.startTransaction(parseFloat(amount), merchantRef, 'ZAR');
506
+ setStatus(`Tap accepted: ${result.resultType}`);
507
+ } catch (e: any) {
508
+ setStatus(`Error: ${e.message}`);
509
+ }
510
+ }
511
+
512
+ return (
513
+ <View style={{ flex: 1, padding: 24, justifyContent: 'center' }}>
514
+ <Text style={{ fontSize: 13, color: '#555', marginBottom: 16 }}>{status}</Text>
515
+
516
+ {!isReady ? (
517
+ <ActivityIndicator size="large" />
518
+ ) : (
519
+ <>
520
+ <TextInput
521
+ placeholder="Amount (e.g. 10.50)"
522
+ value={amount}
523
+ onChangeText={setAmount}
524
+ keyboardType="decimal-pad"
525
+ style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 8, borderRadius: 4 }}
526
+ />
527
+ <TextInput
528
+ placeholder="Merchant reference (e.g. ORDER-001)"
529
+ value={merchantRef}
530
+ onChangeText={setMerchantRef}
531
+ style={{ borderWidth: 1, borderColor: '#ccc', padding: 8, marginBottom: 16, borderRadius: 4 }}
532
+ />
533
+ <TouchableOpacity
534
+ onPress={charge}
535
+ style={{ backgroundColor: '#1976D2', padding: 14, borderRadius: 8, alignItems: 'center' }}>
536
+ <Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>Charge</Text>
537
+ </TouchableOpacity>
538
+ </>
539
+ )}
540
+ </View>
541
+ );
542
+ }
543
+ ```
544
+
545
+ ---
546
+
547
+ ## API Reference
548
+
549
+ ### `HaloSdk.initialize(callbacks, packageName, version, timeout?, animations?)`
550
+
551
+ Initialises the SDK. Must be called before any transaction methods.
552
+
553
+ | Parameter | Type | Default | Description |
554
+ |---|---|---|---|
555
+ | `callbacks` | `IHaloCallbacks` | — | Your callback handler object |
556
+ | `applicationPackageName` | `string` | — | Your app's Android package name |
557
+ | `applicationVersion` | `string` | — | Your app's version string |
558
+ | `onStartTransactionTimeOut` | `number?` | `300000` | Time in ms to wait for a card tap |
559
+ | `enableSchemeAnimations` | `boolean?` | `false` | Show Visa/Mastercard/Amex animations on approval |
560
+
561
+ ### `HaloSdk.startTransaction(amount, reference, currency)`
562
+
563
+ Starts a purchase transaction. Prompts the user to tap their card.
564
+
565
+ | Parameter | Type | Example |
566
+ |---|---|---|
567
+ | `transactionAmount` | `number` | `10.50` |
568
+ | `merchantTransactionReference` | `string` | `'ORDER-001'` |
569
+ | `transactionCurrency` | `string` | `'ZAR'` |
570
+
571
+ Returns `Promise<HaloStartTransactionResult>` — resolves when the card tap is registered. The final outcome arrives via `onHaloTransactionResult`.
572
+
573
+ ### `HaloSdk.cardRefundTransaction(amount, reference, currency)`
574
+
575
+ Starts a card-present refund. Same parameters as `startTransaction`.
576
+
577
+ ### `HaloSdk.cancelTransaction()`
578
+
579
+ Cancels the current in-progress transaction (e.g. if the user presses Cancel while waiting for a tap).
580
+
581
+ ### Callbacks (`IHaloCallbacks`)
582
+
583
+ | Callback | When it fires |
584
+ |---|---|
585
+ | `onInitializationResult(result)` | SDK startup complete (success or failure) |
586
+ | `onHaloTransactionResult(result)` | Final payment outcome; approved, declined, or error |
587
+ | `onHaloUIMessage(message)` | Prompt to show the user during a tap: `PRESENT_CARD`, `PROCESSING`, `APPROVED`, etc. |
588
+ | `onRequestJWT(jwtCallback)` | SDK needs a fresh JWT; call `jwtCallback(yourJwtString)` |
589
+ | `onAttestationError(details)` | Device integrity check failed (rooted device, emulator, etc.) |
590
+ | `onSecurityError(errorCode)` | JWT invalid, merchant revoked, or other security failure |
591
+ | `onCameraControlLost()` | SDK has finished using the camera |
592
+
593
+ ### Result Types
594
+
595
+ `resultType` and `errorCode` are plain strings serialised from native Android enums. Use these known values in your conditional logic.
596
+
597
+ **`HaloInitializationResult.resultType`**
598
+
599
+ | Value | Meaning |
600
+ |---|---|
601
+ | `'Initialized'` | SDK ready; safe to call `startTransaction` |
602
+ | `'RemoteAttestationFailure'` | Cloud attestation failed; inspect `errorCode` for the reason |
603
+ | `'LocalAttestationFailure'` | Device integrity check failed (rooted device, emulator, etc.) |
604
+
605
+ **`HaloInitializationResult.errorCode`** (when `resultType !== 'Initialized'`)
606
+
607
+ | Value | Meaning |
608
+ |---|---|
609
+ | `'OK'` | No error — accompanies `resultType: 'Initialized'` |
610
+ | `'JWTExpired'` | The JWT has expired — generate a fresh one |
611
+
612
+
613
+ **`HaloTransactionResult.resultType`**
614
+
615
+ | Value | Meaning |
616
+ |---|---|
617
+ | `'Approved'` | Transaction approved by the payment network |
618
+ | `'Declined'` | Card declined |
619
+ | `'Cancelled'` | Transaction cancelled (e.g. via `cancelTransaction()`) |
620
+ | `'Error'` | An error occurred — inspect `errorCode` and `errorDetails` |
621
+
622
+ ---
623
+
624
+ ## Documentation
625
+
626
+ Full SDK documentation: [halo-dot-developer-docs.gitbook.io](https://halo-dot-developer-docs.gitbook.io/halo-dot/sdk)
627
+
628
+ ---
629
+
630
+ ## Testing
631
+
632
+ All transactions are void until your NDA is fully executed.
633
+
634
+ 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.
635
+
636
+ ---
637
+
638
+ ## FAQ
639
+
640
+ **Q: How do I set `compileSdkVersion` / `minSdkVersion` if they're not set or causing issues?**
641
+
642
+ You can define them in `android/local.properties` and read them in Gradle:
643
+
644
+ ```properties
645
+ sdk.dir=C\:\\Users\\yourname\\Library\\Android\\Sdk
646
+ aws.accesskey=YOUR_ACCESS_KEY
647
+ aws.secretkey=YOUR_SECRET_KEY
648
+ compileSdkVersion=34
649
+ minSdkVersion=29
650
+ ```
651
+
652
+ ```gradle
653
+ // android/app/build.gradle
654
+ compileSdkVersion localProperties.getProperty('compileSdkVersion').toInteger()
655
+ ```
656
+
657
+ ---
658
+
659
+ **Q: The SDK fails to download / Gradle sync fails.**
660
+
661
+ - Confirm `aws.accesskey` and `aws.secretkey` are in `android/local.properties` with the exact casing shown
662
+ - Open the `android` folder in Android Studio and run **File → Sync Project with Gradle Files**
663
+ - Make sure you accepted the NDA on the developer portal (access is blocked until the NDA is signed)
664
+
665
+ ---
666
+
667
+ **Q: I get a build error about `HaloReactActivity` or `HaloSdkPackage` not found.**
668
+
669
+ - Confirm the npm package is installed: `npm install halo-sdk-react-native`
670
+ - Confirm `MainActivity` extends `HaloReactActivity` (not `ReactActivity`)
671
+ - Run a Gradle sync in Android Studio
672
+
673
+ ---
674
+
675
+ **Q: The SDK initialises but transactions never complete.**
676
+
677
+ - Check that all [AndroidManifest permissions](#androidmanifest-permissions) are declared, especially the NFC `intent-filter` block
678
+ - Check that the NFC intent-filter is inside the `<activity>` block for `MainActivity`
679
+ - Verify your JWT is valid using `POST https://kernelserver.qa.haloplus.io/<sdk-version>/tokens/checkjwt`
680
+ - Check that `minSdkVersion` ≥ 29 and `compileSdkVersion` / `targetSdkVersion` ≥ 34
681
+
682
+ ---
683
+
684
+ **Q: How do I get a JWT for testing?**
685
+
686
+ 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`.
687
+
688
+ ---
689
+
690
+ **Q: Manifest merger fails with an attribute conflict (e.g. `android:label`, `android:allowBackup`).**
691
+
692
+ The Halo SDK bundles several sub-libraries (Visa Sensory Branding, etc.), each with their own `AndroidManifest.xml`. Any `processDebugMainManifest` failure caused by an attribute clash is fixed by adding the conflicting attribute name to `tools:replace` on your `<application>` element:
693
+
694
+ ```xml
695
+ <application
696
+ ...
697
+ tools:replace="android:label,android:allowBackup">
698
+ ```
699
+
700
+ If you add a new attribute to `tools:replace` and the **same error persists on the very next build**, Gradle may have cached the previously failed manifest merge. Run a clean build and try again:
701
+
702
+ ```bash
703
+ cd android && ./gradlew clean
704
+ ```
705
+
706
+ Then re run your normal build (`npx react-native run-android` or Android Studio).
707
+
708
+ ---
709
+
710
+ **Q: TypeScript build errors about `customConditions` or `moduleResolution` after editing `tsconfig.json`.**
711
+
712
+ Do not override `moduleResolution` in your project's `tsconfig.json`. The base `@react-native/typescript-config` sets `"moduleResolution": "bundler"`, which is the only value compatible with its `customConditions` setting. Overriding it to `"node"` causes a TypeScript error.
713
+
714
+ Instead, extend the base config and only add project specific overrides:
715
+
716
+ ```json
717
+ {
718
+ "extends": "@react-native/typescript-config/tsconfig.json",
719
+ "compilerOptions": {
720
+ "skipLibCheck": true
721
+ }
722
+ }
723
+ ```
724
+
725
+ `skipLibCheck: true` suppresses spurious type errors that originate inside `node_modules` (e.g. phantom `@types/react` v19 conflicts) without changing how your own code is compiled.
726
+
727
+ ---
728
+
729
+ **Q: `onInitializationResult` fires with `resultType: 'RemoteAttestationFailure'` and `errorCode: 'JWTExpired'`.**
730
+
731
+ 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.
732
+
733
+ If the second successful callback never arrives:
734
+ 1. Generate a fresh token and update `Config.tempJwt`
735
+ 2. Make sure your `onRequestJWT` callback calls `jwtCallback(yourJwt)` synchronously — async calls are not supported without extra handling