@icedevml/react-native-host-card-emulation 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +87 -0
- package/README.md +216 -152
- package/RTNHCEModuleIOS.podspec +1 -1
- package/android/build.gradle +5 -17
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/BinaryUtils.java +11 -5
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEManager.java +558 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +22 -299
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +232 -166
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +5 -4
- package/ios/RTNHCEModuleIOS.mm +69 -48
- package/ios/RTNHCEModuleIOSImpl.swift +429 -100
- package/js/NativeHCEModule.ts +48 -130
- package/js/background.ts +168 -0
- package/js/index.ts +49 -0
- package/js/internal.ts +218 -0
- package/js/session.ts +192 -0
- package/js/types.ts +179 -0
- package/package.json +19 -3
- package/react-native-spm-prefix.h +10 -0
- package/android/src/main/AndroidMainfest.xml +0 -3
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +0 -7
- package/js/hceBackground.ts +0 -68
package/js/session.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import NativeHCEModule, {NativeHCEEvent} from './NativeHCEModule';
|
|
2
|
+
import {
|
|
3
|
+
addSessionListener,
|
|
4
|
+
callHandler,
|
|
5
|
+
HCEError,
|
|
6
|
+
processApdu,
|
|
7
|
+
reportError,
|
|
8
|
+
resolveHandlerOptions,
|
|
9
|
+
SerialQueue,
|
|
10
|
+
} from './internal';
|
|
11
|
+
import type {BeginSessionOptions, EmulationStopStatus, HCESession, SessionHandlers} from './types';
|
|
12
|
+
|
|
13
|
+
let sessionCounter = 0;
|
|
14
|
+
|
|
15
|
+
function generateSessionId(): string {
|
|
16
|
+
sessionCounter += 1;
|
|
17
|
+
return `fg-${Date.now().toString(36)}-${sessionCounter.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class Session implements HCESession {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly closed: Promise<string>;
|
|
23
|
+
readonly started: Promise<void>;
|
|
24
|
+
|
|
25
|
+
private readonly handlers: SessionHandlers;
|
|
26
|
+
private readonly handlerTimeout: number;
|
|
27
|
+
private readonly queue = new SerialQueue();
|
|
28
|
+
private readonly removeListener: () => void;
|
|
29
|
+
private active = true;
|
|
30
|
+
private isStarted = false;
|
|
31
|
+
private resolveClosed!: (reason: string) => void;
|
|
32
|
+
private resolveStarted!: () => void;
|
|
33
|
+
private rejectStarted!: (error: Error) => void;
|
|
34
|
+
|
|
35
|
+
constructor(id: string, handlers: SessionHandlers, handlerTimeout: number) {
|
|
36
|
+
this.id = id;
|
|
37
|
+
this.handlers = handlers;
|
|
38
|
+
this.handlerTimeout = handlerTimeout;
|
|
39
|
+
this.closed = new Promise(resolve => {
|
|
40
|
+
this.resolveClosed = resolve;
|
|
41
|
+
});
|
|
42
|
+
this.started = new Promise((resolve, reject) => {
|
|
43
|
+
this.resolveStarted = resolve;
|
|
44
|
+
this.rejectStarted = reject;
|
|
45
|
+
});
|
|
46
|
+
// beginSession() handles the rejection, avoid unhandled rejection warnings when it doesn't wait for it
|
|
47
|
+
this.started.catch(() => {});
|
|
48
|
+
// registered before the native session is created, so that no event can be missed
|
|
49
|
+
this.removeListener = addSessionListener(id, event => this.handleEvent(event));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get isActive(): boolean {
|
|
53
|
+
return this.active;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private runHandler(name: string, handler: (() => unknown) | undefined) {
|
|
57
|
+
if (!handler) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.queue
|
|
62
|
+
.push(() => callHandler(name, handler, this.handlerTimeout))
|
|
63
|
+
.catch(e => reportError(this.handlers.onError, e));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private handleEvent(event: NativeHCEEvent) {
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case 'sessionStarted':
|
|
69
|
+
this.isStarted = true;
|
|
70
|
+
this.resolveStarted();
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'readerDetected':
|
|
74
|
+
this.runHandler('onReaderDetected()', this.handlers.onReaderDetected);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'readerLost':
|
|
78
|
+
this.runHandler('onReaderLost()', this.handlers.onReaderLost);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'apdu':
|
|
82
|
+
this.queue.push(() =>
|
|
83
|
+
processApdu(this.id, event, this.handlers.onApdu, this.handlers.onError, this.handlerTimeout),
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'sessionInvalidated':
|
|
88
|
+
this.close(event.reason || 'unknown');
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
close(reason: string) {
|
|
94
|
+
if (!this.active) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.active = false;
|
|
99
|
+
this.removeListener();
|
|
100
|
+
|
|
101
|
+
if (!this.isStarted) {
|
|
102
|
+
// beginSession() rejects, so the app never gets the session object and isn't notified
|
|
103
|
+
this.rejectStarted(new HCEError('err_session_invalidated', `The session was invalidated: ${reason}`));
|
|
104
|
+
} else {
|
|
105
|
+
const onInvalidated = this.handlers.onInvalidated;
|
|
106
|
+
this.runHandler('onInvalidated()', onInvalidated ? () => onInvalidated(reason) : undefined);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.resolveClosed(reason);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private ensureActive() {
|
|
113
|
+
if (!this.active) {
|
|
114
|
+
throw new HCEError('err_no_session', 'The session is not active.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async startEmulation(): Promise<void> {
|
|
119
|
+
this.ensureActive();
|
|
120
|
+
await NativeHCEModule.startEmulation(this.id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async stopEmulation(status: EmulationStopStatus = 'success'): Promise<void> {
|
|
124
|
+
this.ensureActive();
|
|
125
|
+
await NativeHCEModule.stopEmulation(this.id, status);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async isEmulationRunning(): Promise<boolean> {
|
|
129
|
+
if (!this.active) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return NativeHCEModule.isEmulationRunning(this.id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setAlertMessage(message: string): void {
|
|
137
|
+
if (this.active) {
|
|
138
|
+
NativeHCEModule.setSessionAlertMessage(this.id, message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
invalidate(): void {
|
|
143
|
+
if (!this.active) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
NativeHCEModule.invalidateSession(this.id);
|
|
148
|
+
// don't rely on the native event, the session is closed from now on
|
|
149
|
+
this.close('userInvalidated');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Begin the HCE session. Resolves once the session is started and ready for startEmulation().
|
|
155
|
+
* Only a single session can be active at a time.
|
|
156
|
+
*/
|
|
157
|
+
export async function beginSession(handlers: SessionHandlers, options: BeginSessionOptions = {}): Promise<HCESession> {
|
|
158
|
+
if (!handlers || typeof handlers.onApdu !== 'function') {
|
|
159
|
+
throw new HCEError('err_invalid_argument', 'handlers.onApdu must be a function.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const {signal} = options;
|
|
163
|
+
const {handlerTimeout, allowWhenLocked} = resolveHandlerOptions(options);
|
|
164
|
+
|
|
165
|
+
if (signal?.aborted) {
|
|
166
|
+
throw new HCEError('err_aborted', 'The operation was aborted.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const session = new Session(generateSessionId(), handlers, handlerTimeout);
|
|
170
|
+
const onAbort = () => session.invalidate();
|
|
171
|
+
signal?.addEventListener('abort', onAbort);
|
|
172
|
+
session.closed.then(() => signal?.removeEventListener('abort', onAbort));
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await NativeHCEModule.beginSession(session.id, allowWhenLocked);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
session.close('beginSessionFailed');
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await session.started;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (signal?.aborted) {
|
|
185
|
+
throw new HCEError('err_aborted', 'The operation was aborted.');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return session;
|
|
192
|
+
}
|
package/js/types.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export type HCEPlatform = 'android' | 'ios';
|
|
2
|
+
|
|
3
|
+
export interface HCECapabilities {
|
|
4
|
+
readonly platform: HCEPlatform;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the device and the OS version support host card emulation at all.
|
|
7
|
+
*/
|
|
8
|
+
readonly hceSupported: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether NFC is currently enabled (Android) / available for reading (iOS).
|
|
11
|
+
*/
|
|
12
|
+
readonly nfcEnabled: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the app is allowed to use HCE right now.
|
|
15
|
+
* iOS: result of CardSession.isEligible (region, entitlement, etc.).
|
|
16
|
+
* Android: same as hceSupported.
|
|
17
|
+
*/
|
|
18
|
+
readonly eligible: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether registerBackgroundHandler() is supported (Android only).
|
|
21
|
+
*/
|
|
22
|
+
readonly backgroundSupported: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether acquireExclusiveNFC() is supported (iOS only).
|
|
25
|
+
*/
|
|
26
|
+
readonly presentmentSupported: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CommandAPDU {
|
|
30
|
+
/**
|
|
31
|
+
* Identifier of the C-APDU, unique within the app process.
|
|
32
|
+
*/
|
|
33
|
+
readonly id: number;
|
|
34
|
+
/**
|
|
35
|
+
* Raw bytes of the C-APDU.
|
|
36
|
+
*/
|
|
37
|
+
readonly command: Uint8Array;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the device was locked when the C-APDU was received.
|
|
40
|
+
* iOS: always false, it's impossible to communicate over HCE while the device is locked.
|
|
41
|
+
*/
|
|
42
|
+
readonly deviceLocked: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Raw bytes of the R-APDU, including the status word (at least 2 bytes).
|
|
47
|
+
*/
|
|
48
|
+
export type ResponseAPDU = Uint8Array | ReadonlyArray<number>;
|
|
49
|
+
|
|
50
|
+
export type ApduHandler = (apdu: CommandAPDU) => ResponseAPDU | Promise<ResponseAPDU>;
|
|
51
|
+
|
|
52
|
+
export type EmulationStopStatus = 'success' | 'failure';
|
|
53
|
+
|
|
54
|
+
export interface SessionHandlers {
|
|
55
|
+
/**
|
|
56
|
+
* Handle the C-APDU and return the R-APDU. Called sequentially, the next C-APDU is never
|
|
57
|
+
* delivered before the previous call has finished. If the handler throws, returns an invalid
|
|
58
|
+
* response or doesn't finish within the handler timeout, the reader receives the 6F00 status word
|
|
59
|
+
* (no precise diagnosis).
|
|
60
|
+
*/
|
|
61
|
+
onApdu: ApduHandler;
|
|
62
|
+
/**
|
|
63
|
+
* iOS: The reader's field was detected.
|
|
64
|
+
* Android: The reader has selected the application.
|
|
65
|
+
*/
|
|
66
|
+
onReaderDetected?: () => void | Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* The reader was lost, a different application was selected by the reader or the emulation was stopped.
|
|
69
|
+
*/
|
|
70
|
+
onReaderLost?: () => void | Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* The session was closed and can't be used anymore.
|
|
73
|
+
*/
|
|
74
|
+
onInvalidated?: (reason: string) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Errors thrown by the handlers above (including the err_handler_timeout errors)
|
|
77
|
+
* or by responding to a C-APDU. Logged with console.warn() if not provided.
|
|
78
|
+
*/
|
|
79
|
+
onError?: (error: unknown) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface HandlerOptions {
|
|
83
|
+
/**
|
|
84
|
+
* The maximum time (in milliseconds) a single handler call may take, including the returned promise.
|
|
85
|
+
* Once it's exceeded, the handler's result is ignored, onError() receives an err_handler_timeout error
|
|
86
|
+
* and the next event is delivered (a timed out onApdu() is responded to with the 6F00 status word).
|
|
87
|
+
* Without the limit, a handler that never finishes would block all the subsequent events.
|
|
88
|
+
*
|
|
89
|
+
* Default: 10000. Pass 0 or Infinity to disable the timeout.
|
|
90
|
+
*/
|
|
91
|
+
handlerTimeout?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Android: Whether the interaction is allowed while the device is locked. If false, the C-APDUs
|
|
94
|
+
* received while the device is locked are never delivered to the handlers and the reader receives
|
|
95
|
+
* an error status word instead. Set it to false unless the app is meant to work on the lock screen.
|
|
96
|
+
* iOS: Ignored, it's impossible to communicate over HCE while the device is locked.
|
|
97
|
+
*
|
|
98
|
+
* Default: true.
|
|
99
|
+
*/
|
|
100
|
+
allowWhenLocked?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface BeginSessionOptions extends HandlerOptions {
|
|
104
|
+
/**
|
|
105
|
+
* Aborting the signal invalidates the session, also if it's still being started.
|
|
106
|
+
*/
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The options of the background handler, handlerTimeout also applies to the factory.
|
|
112
|
+
*/
|
|
113
|
+
export type BackgroundHandlerOptions = HandlerOptions;
|
|
114
|
+
|
|
115
|
+
export interface HCESession {
|
|
116
|
+
readonly id: string;
|
|
117
|
+
/**
|
|
118
|
+
* Whether the session is still open.
|
|
119
|
+
*/
|
|
120
|
+
readonly isActive: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Resolves with the reason once the session is closed.
|
|
123
|
+
*/
|
|
124
|
+
readonly closed: Promise<string>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* iOS: Start the card emulation, displaying the system NFC UI.
|
|
128
|
+
* Android: Start forwarding C-APDUs to this session.
|
|
129
|
+
*/
|
|
130
|
+
startEmulation(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* iOS: Stop the card emulation with "success"/"failure" UI status.
|
|
133
|
+
* Android: Stop forwarding C-APDUs to this session (the status is ignored).
|
|
134
|
+
*/
|
|
135
|
+
stopEmulation(status?: EmulationStopStatus): Promise<void>;
|
|
136
|
+
isEmulationRunning(): Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* iOS: Set the alert message of the system NFC UI.
|
|
139
|
+
* Android: No-op.
|
|
140
|
+
*/
|
|
141
|
+
setAlertMessage(message: string): void;
|
|
142
|
+
/**
|
|
143
|
+
* Close the session. Idempotent.
|
|
144
|
+
*/
|
|
145
|
+
invalidate(): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* - 'background': the app is not in foreground (or not running at all).
|
|
150
|
+
* - 'foreground': the app is in foreground, but it has no session with running emulation.
|
|
151
|
+
*/
|
|
152
|
+
export type BackgroundAppState = 'background' | 'foreground';
|
|
153
|
+
|
|
154
|
+
export interface BackgroundContext {
|
|
155
|
+
readonly sessionId: string;
|
|
156
|
+
readonly appState: BackgroundAppState;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface BackgroundHandlers {
|
|
160
|
+
/**
|
|
161
|
+
* Same semantics as SessionHandlers.onApdu.
|
|
162
|
+
*/
|
|
163
|
+
onApdu: ApduHandler;
|
|
164
|
+
/**
|
|
165
|
+
* The reader was lost or a different application was selected by the reader.
|
|
166
|
+
* The background interaction ends after this handler has finished.
|
|
167
|
+
*/
|
|
168
|
+
onReaderLost?: () => void | Promise<void>;
|
|
169
|
+
onError?: (error: unknown) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Called once per interaction with the reader, so the returned handlers can safely keep
|
|
174
|
+
* the state of that interaction in a closure. If it throws or doesn't finish within the handler
|
|
175
|
+
* timeout, the reader receives an error status word for the rest of the interaction.
|
|
176
|
+
*/
|
|
177
|
+
export type BackgroundHandlerFactory = (
|
|
178
|
+
context: BackgroundContext,
|
|
179
|
+
) => BackgroundHandlers | Promise<BackgroundHandlers>;
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icedevml/react-native-host-card-emulation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Host Card Emulation (HCE) React Native Turbo Module for iOS and Android platforms.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"main": "js/index.ts",
|
|
6
|
+
"types": "js/index.ts",
|
|
7
|
+
"react-native": "js/index.ts",
|
|
8
|
+
"source": "js/index.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
7
12
|
"files": [
|
|
8
13
|
"js",
|
|
9
14
|
"android",
|
|
10
15
|
"ios",
|
|
11
16
|
"RTNHCEModuleIOS.podspec",
|
|
17
|
+
"Package.swift",
|
|
18
|
+
"react-native-spm-prefix.h",
|
|
12
19
|
"!android/build",
|
|
13
20
|
"!ios/build",
|
|
14
21
|
"!**/__tests__",
|
|
@@ -32,6 +39,15 @@
|
|
|
32
39
|
"react": "*",
|
|
33
40
|
"react-native": "*"
|
|
34
41
|
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@babel/core": "^7.25.2",
|
|
44
|
+
"@react-native/babel-preset": "0.88.1",
|
|
45
|
+
"@react-native/jest-preset": "0.88.1",
|
|
46
|
+
"@types/jest": "^29.5.13",
|
|
47
|
+
"jest": "^29.6.3",
|
|
48
|
+
"react": "19.3.0",
|
|
49
|
+
"react-native": "0.88.1"
|
|
50
|
+
},
|
|
35
51
|
"codegenConfig": {
|
|
36
52
|
"name": "RTNHCEModuleSpec",
|
|
37
53
|
"type": "modules",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-SCAFFOLDED by react-native spm scaffold — mirrors CocoaPods' default
|
|
2
|
+
// prefix header so ObjC sources that rely on an implicit Foundation/UIKit
|
|
3
|
+
// import compile under SPM (which has no prefix-header mechanism). Safe to
|
|
4
|
+
// delete + regenerate via `npx react-native spm scaffold`.
|
|
5
|
+
#ifdef __OBJC__
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
#if __has_include(<UIKit/UIKit.h>)
|
|
8
|
+
#import <UIKit/UIKit.h>
|
|
9
|
+
#endif
|
|
10
|
+
#endif
|
package/js/hceBackground.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import NativeHCEModule, {HCEModuleBackgroundEvent} from "./NativeHCEModule";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Android: Data passed by the native side to the 'handleBackgroundHCECall' headless task.
|
|
5
|
-
* Pass it as-is to createBackgroundHCE().
|
|
6
|
-
*/
|
|
7
|
-
export interface BackgroundHCETaskData {
|
|
8
|
-
/**
|
|
9
|
-
* Handle of the background HCE session.
|
|
10
|
-
*/
|
|
11
|
-
handle: string,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface BackgroundEventHandler {
|
|
15
|
-
(
|
|
16
|
-
event: HCEModuleBackgroundEvent,
|
|
17
|
-
respondAPDU: (rapdu: string) => Promise<void>,
|
|
18
|
-
taskData: BackgroundHCETaskData,
|
|
19
|
-
): Promise<void>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ProcessBackgroundHCEFunc {
|
|
23
|
-
(
|
|
24
|
-
handler: BackgroundEventHandler
|
|
25
|
-
): void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Set up handling of a background HCE session.
|
|
30
|
-
*
|
|
31
|
-
* @param taskData Data received by the 'handleBackgroundHCECall' headless task. A bare handle
|
|
32
|
-
* string is accepted as well, for backwards compatibility.
|
|
33
|
-
*/
|
|
34
|
-
export const createBackgroundHCE = (taskData: BackgroundHCETaskData | string): ProcessBackgroundHCEFunc => {
|
|
35
|
-
const useTaskData: BackgroundHCETaskData = typeof taskData === "string"
|
|
36
|
-
? {handle: taskData}
|
|
37
|
-
: taskData;
|
|
38
|
-
const handle = useTaskData.handle;
|
|
39
|
-
|
|
40
|
-
const respondAPDU = (rapdu: string) => {
|
|
41
|
-
return NativeHCEModule.respondAPDU(handle, rapdu);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (handler: BackgroundEventHandler) => {
|
|
45
|
-
const subscription = NativeHCEModule.onBackgroundEvent(async (event) => {
|
|
46
|
-
if (event.audience !== handle) {
|
|
47
|
-
// ignore
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
await handler(event, respondAPDU, useTaskData);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
throw e;
|
|
55
|
-
} finally {
|
|
56
|
-
if (event.type === "readerDeselected") {
|
|
57
|
-
subscription.remove();
|
|
58
|
-
NativeHCEModule.finishBackgroundHCE(handle);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (!NativeHCEModule.beginBackgroundHCE(handle)) {
|
|
64
|
-
subscription.remove();
|
|
65
|
-
NativeHCEModule.finishBackgroundHCE(handle);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|