lig-scanner-expo-sdk 0.5.0 → 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/CLAUDE.md CHANGED
@@ -12,7 +12,7 @@ This is `lig-scanner-expo-sdk`, an Expo module SDK wrapper for the LiG Scanner l
12
12
  - **Core TypeScript API**: `src/` contains the main module interface and high-level components
13
13
  - **Native module interface**: `LigScannerExpoSdkModule.ts` declares native methods (initialize, startScanning, stopScanning, showARPlayer)
14
14
  - **Native iOS implementation**: `ios/` (Swift) with LiG Scanner engine integration
15
- - **Native Android implementation**: `android/` (Kotlin) with corresponding functionality
15
+ - **Native Android implementation**: `android/` (Kotlin) with corresponding functionality
16
16
  - **Example app**: `example/App.tsx` demonstrates both integrated and manual usage patterns
17
17
 
18
18
  ### Key Components Architecture
@@ -60,6 +60,12 @@ npm run open:ios
60
60
 
61
61
  # Open Android project in Android Studio
62
62
  npm run open:android
63
+
64
+ # Run example app on iOS (requires iOS simulator or device)
65
+ npm run ios
66
+
67
+ # Run example app on Android (requires Android emulator or device)
68
+ npm run android
63
69
  ```
64
70
 
65
71
  ## Development Patterns
@@ -69,7 +75,7 @@ The SDK uses a state machine pattern managed by `LigScannerManager`:
69
75
  - `initializing` → `ready` → `scanning` → `ligtag_ready` (success path)
70
76
  - Error states: `error_device`, `error_auth`, `error_other`
71
77
 
72
- ### Event System Architecture
78
+ ### Event System Architecture
73
79
  - **Native Events**: `onLigtagFound`, `onStatusReported`, `onARReady`, `onARError`, `onARClosed`, `onGameEvent`
74
80
  - **Manager Events**: `stateChange`, `tagUpdate`, `tagReady`, `error`
75
81
  - **Hook Integration**: State changes automatically trigger React re-renders
@@ -93,4 +99,4 @@ The `example/App.tsx` demonstrates two complete integration approaches:
93
99
 
94
100
  - `expo-module.config.json`: Defines Apple/Android platforms, native module class names, and required permissions (camera)
95
101
  - `tsconfig.json`: Extends expo-module-scripts base config, outputs to `build/`
96
- - Platform permissions: Camera access required for both iOS (NSCameraUsageDescription) and Android (android.permission.CAMERA)
102
+ - Platform permissions: Camera access required for both iOS (NSCameraUsageDescription) and Android (android.permission.CAMERA)
File without changes
@@ -0,0 +1,2 @@
1
+ #Sat Dec 13 20:46:53 CST 2025
2
+ gradle.version=8.9
File without changes
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'tw.com.lig.scanner.rn.expo'
4
- version = '0.1.0'
4
+ version = '0.5.1'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -46,7 +46,7 @@ android {
46
46
  namespace "tw.com.lig.scanner.rn.expo"
47
47
  defaultConfig {
48
48
  versionCode 1
49
- versionName "0.1.0"
49
+ versionName "0.5.1"
50
50
  }
51
51
  lintOptions {
52
52
  abortOnError false
@@ -258,7 +258,7 @@ class ARPlayerView: ExpoView {
258
258
  guard !isArOriginSet,
259
259
  let lightTagId = lightTagId,
260
260
  let arSceneView = arSceneView,
261
- let sceneContext = sceneContext else {
261
+ sceneContext != nil else {
262
262
  return
263
263
  }
264
264
 
@@ -296,13 +296,18 @@ class ARPlayerView: ExpoView {
296
296
 
297
297
  Task {
298
298
  do {
299
+
299
300
  // Load scene information from LiG Cloud (following iOS Player SDK pattern)
300
301
  let coordinateSystem = try await sceneContext.loadCoordinateSystemFrom(lightId: deviceId, accessToken: scannerAccessToken ?? "")
301
302
 
302
303
  if let cs = coordinateSystem, let scene = cs.scenes?.first {
304
+
303
305
  sceneContext.startDownloadZip(with: scene.assetsUrl)
306
+ } else {
307
+
304
308
  }
305
309
  } catch {
310
+
306
311
  // Handle loading error silently
307
312
  }
308
313
  }
@@ -367,14 +372,18 @@ extension ARPlayerView: ARSCNViewDelegate {
367
372
 
368
373
  extension ARPlayerView: SceneKitContextDelegate {
369
374
  func didLoaded() {
370
- // let ligScene = LiGPlayer.sharedContext?.ligScene
371
- // onSceneLoaded([
372
- // "scene": [
373
- // "id": ligScene?.scene?.sceneID?.description ?? "",
374
- // "name": ligScene?.scene?.name ?? "",
375
- // "status": "loaded"
376
- // ]
377
- // ])
375
+
376
+
377
+ // Cannot access ligScene.scene (internal), so passing empty info for now
378
+ let sceneData: [String: Any] = [
379
+ "id": "",
380
+ "name": "",
381
+ "status": "loaded"
382
+ ]
383
+
384
+ onSceneLoaded([
385
+ "scene": sceneData
386
+ ])
378
387
  }
379
388
 
380
389
  func gameTypeAction(gameType: LiGPlayerKit.GameType, value: LiGPlayerKit.GameResponse) {
@@ -21,7 +21,7 @@ Pod::Spec.new do |s|
21
21
  s.dependency 'ExpoModulesCore'
22
22
 
23
23
  # LiG Scanner SDK dependencies
24
- s.dependency 'LiGScannerKit', '~> 5.1.4'
24
+ s.dependency 'LiGScannerKit', '~> 5.2.1'
25
25
  s.dependency 'LiGPlayerKit', '~> 1.8.15'
26
26
 
27
27
  # iOS specific frameworks
@@ -6,8 +6,9 @@ import UIKit
6
6
  public class LigScannerExpoSdkModule: Module {
7
7
  private var isScanning = false
8
8
  private var isInitialized = false
9
+ private var shouldStartWhenReady = false
9
10
  private let scanner = LiGScanner.shared
10
- private var lastDetectedTag: LiGTag?
11
+ private var lastDetectedTag: LigTag?
11
12
  private var hasListeners = false
12
13
 
13
14
  // Each module class must implement the definition function. The definition consists of components
@@ -71,24 +72,40 @@ public class LigScannerExpoSdkModule: Module {
71
72
 
72
73
  AsyncFunction("startScanning") { (promise: Promise) in
73
74
  guard self.isInitialized else {
75
+ self.shouldStartWhenReady = true
74
76
  promise.reject("NOT_INITIALIZED", "Scanner not initialized. Call initialize() first.")
75
77
  return
76
78
  }
77
-
79
+
78
80
  guard !self.isScanning else {
79
81
  promise.resolve("Already scanning")
80
82
  return
81
83
  }
82
-
83
- self.scanner.start()
84
- self.isScanning = true
85
-
86
- self.sendEvent("onStatusReported", [
87
- "status": "SCANNING",
88
- "value": 2
89
- ])
90
-
91
- promise.resolve("Scanning started")
84
+
85
+ Task {
86
+ await MainActor.run {
87
+ // Start LiGTransformManager early for gravity alignment
88
+ LiGTransformManager.shared.start()
89
+ }
90
+
91
+ do {
92
+ try await self.scanner.start()
93
+ await MainActor.run {
94
+ self.isScanning = true
95
+
96
+ self.sendEvent("onStatusReported", [
97
+ "status": "SCANNING",
98
+ "value": 2
99
+ ])
100
+
101
+ promise.resolve("Scanning started")
102
+ }
103
+ } catch {
104
+ await MainActor.run {
105
+ promise.reject("START_FAILED", "Failed to start scanner: \(error)")
106
+ }
107
+ }
108
+ }
92
109
  }
93
110
 
94
111
  AsyncFunction("stopScanning") { (promise: Promise) in
@@ -98,6 +115,9 @@ public class LigScannerExpoSdkModule: Module {
98
115
  }
99
116
 
100
117
  self.scanner.stop()
118
+ // Stop LiGTransformManager
119
+ LiGTransformManager.shared.stop()
120
+
101
121
  self.isScanning = false
102
122
 
103
123
  promise.resolve("Scanning stopped")
@@ -150,58 +170,86 @@ public class LigScannerExpoSdkModule: Module {
150
170
  }
151
171
 
152
172
  // MARK: - Helper Methods
153
-
173
+
154
174
  private func initializeScanner(_ productKey: String, _ promise: Promise) {
155
- self.isInitialized = true
156
-
157
175
  // Set up delegate
158
176
  self.scanner.delegate = self
159
-
177
+
160
178
  self.sendEvent("onStatusReported", [
161
179
  "status": "INITIALIZING",
162
180
  "value": 0
163
181
  ])
164
-
165
- // Initialize LiGScanner SDK
166
- self.scanner.initialize(productKey)
167
-
168
- self.sendEvent("onStatusReported", [
169
- "status": "INITIALIZED",
170
- "value": 1
171
- ])
172
-
173
- promise.resolve("Scanner initialized")
182
+
183
+ Task {
184
+ do {
185
+ // Initialize LiGScanner SDK (async/await)
186
+ try await self.scanner.initialize(productKey: productKey)
187
+
188
+ await MainActor.run {
189
+ self.isInitialized = true
190
+
191
+ self.sendEvent("onStatusReported", [
192
+ "status": "INITIALIZED",
193
+ "value": 1
194
+ ])
195
+
196
+ // Start scanning if it was requested before initialization completed
197
+ if self.shouldStartWhenReady {
198
+ self.shouldStartWhenReady = false
199
+ Task {
200
+ do {
201
+ try await self.scanner.start()
202
+ await MainActor.run {
203
+ self.isScanning = true
204
+
205
+ // Start LiGTransformManager for gravity alignment
206
+ LiGTransformManager.shared.start()
207
+
208
+ self.sendEvent("onStatusReported", [
209
+ "status": "SCANNING",
210
+ "value": 2
211
+ ])
212
+ }
213
+ } catch {
214
+ // Handle start error silently
215
+ }
216
+ }
217
+ }
218
+
219
+ promise.resolve("Scanner initialized")
220
+ }
221
+ } catch {
222
+ await MainActor.run {
223
+ self.isInitialized = false
224
+ promise.reject("INIT_FAILED", "Failed to initialize scanner: \(error)")
225
+ }
226
+ }
227
+ }
174
228
  }
175
229
  }
176
230
 
177
231
  // MARK: - LiGScannerDelegate
178
232
 
179
233
  extension LigScannerExpoSdkModule: LiGScannerDelegate {
180
- public func scanner(_ scanner: LiGScanner, didDetect ligtag: LiGTag?) {
181
- guard let tag = ligtag else { return }
182
-
234
+ public func ligScanner(_ scanner: LiGScanner, didFind ligTag: LigTag?) {
235
+ guard let tag = ligTag else { return }
236
+
183
237
  // Store the tag for AR player
184
238
  lastDetectedTag = tag
185
-
186
-
187
- // Map LiGTag to match Android's onLightIDFound format exactly
239
+
240
+
241
+ // Map LigTag to match Android's onLightIDFound format exactly
188
242
  var eventData: [String: Any] = [
189
243
  "x": Double(tag.coordinateX),
190
244
  "y": Double(tag.coordinateY),
191
245
  "isReady": tag.isReady,
192
- "isDetected": tag.isDetected,
246
+ "isDetected": tag.detected,
193
247
  "deviceId": tag.deviceId,
194
- "status": mapStatusToString(tag.status)
248
+ "status": mapStatusFromBooleans(detected: tag.detected, decoded: tag.decoded, validPose: tag.validPose, isReady: tag.isReady)
195
249
  ]
196
-
197
- // Add timing data if detected
198
- if tag.isDetected {
199
- eventData["detectionTime"] = Int(tag.detectionTime)
200
- eventData["decodedTime"] = Int(tag.decodedTime)
201
- }
202
-
250
+
203
251
  // Add position data for all detected tags (needed for AR coordinate system setup)
204
- if tag.isDetected {
252
+ if tag.detected {
205
253
  eventData["rotation"] = [
206
254
  "x": Double(tag.rotation.x),
207
255
  "y": Double(tag.rotation.y),
@@ -212,126 +260,96 @@ extension LigScannerExpoSdkModule: LiGScannerDelegate {
212
260
  "y": Double(tag.translation.y),
213
261
  "z": Double(tag.translation.z)
214
262
  ]
215
- eventData["position"] = [
216
- "x": Double(tag.position.x),
217
- "y": Double(tag.position.y),
218
- "z": Double(tag.position.z)
219
- ]
220
- // Add accessToken for AR cloud authentication
221
- eventData["accessToken"] = tag.accessToken
222
-
263
+ // Add accessToken for AR cloud authentication (now optional)
264
+ if let accessToken = tag.accessToken {
265
+ eventData["accessToken"] = accessToken
266
+ }
267
+
223
268
  // Add transform matrix for direct AR coordinate system setup
224
- let transform = tag.transform
225
- let transformArray: [Double] = [
226
- Double(transform.columns.0.x), Double(transform.columns.0.y),
227
- Double(transform.columns.0.z), Double(transform.columns.0.w),
228
- Double(transform.columns.1.x), Double(transform.columns.1.y),
229
- Double(transform.columns.1.z), Double(transform.columns.1.w),
230
- Double(transform.columns.2.x), Double(transform.columns.2.y),
231
- Double(transform.columns.2.z), Double(transform.columns.2.w),
232
- Double(transform.columns.3.x), Double(transform.columns.3.y),
233
- Double(transform.columns.3.z), Double(transform.columns.3.w)
234
- ]
235
- eventData["transform"] = transformArray
269
+ // Use gravity-aligned transform from LiGTransformManager ONLY if pose is valid and ready
270
+ // This prevents Division by Zero in simd_quaternion when rotation is (0,0,0)
271
+ if tag.isReady && tag.validPose {
272
+ let transform = LiGTransformManager.shared.getAlignedTransform(tag: tag)
273
+
274
+ // Check for NaNs as a second safety layer
275
+ if !transform.columns.0.x.isNaN && !transform.columns.0.y.isNaN {
276
+ let transformArray: [Double] = [
277
+ Double(transform.columns.0.x), Double(transform.columns.0.y),
278
+ Double(transform.columns.0.z), Double(transform.columns.0.w),
279
+ Double(transform.columns.1.x), Double(transform.columns.1.y),
280
+ Double(transform.columns.1.z), Double(transform.columns.1.w),
281
+ Double(transform.columns.2.x), Double(transform.columns.2.y),
282
+ Double(transform.columns.2.z), Double(transform.columns.2.w),
283
+ Double(transform.columns.3.x), Double(transform.columns.3.y),
284
+ Double(transform.columns.3.z), Double(transform.columns.3.w)
285
+ ]
286
+ eventData["transform"] = transformArray
287
+ }
288
+ }
236
289
  }
237
-
290
+
238
291
  sendEvent("onLigtagFound", eventData)
239
292
  }
240
293
 
241
- public func scanner(_ scanner: LiGScannerKit.LiGScanner, didReport status: LiGScannerKit.LiGScannerError) {
242
-
294
+ public func ligScanner(_ scanner: LiGScannerKit.LiGScanner, didReport status: LiGScannerKit.StatusCode) {
295
+
243
296
  // Handle specific status codes
244
297
  switch status {
245
- case .correct, .deviceIsSupported, .authenticationOK, .initializationComplete, .scannerStopped:
298
+ case .deviceIsSupported, .authenticationOK, .scannerStopped:
246
299
  // These are positive status updates, not errors
247
300
  sendEvent("onStatusReported", [
248
- "status": mapErrorToString(status),
301
+ "status": mapStatusCodeToString(status),
249
302
  "value": status.rawValue,
250
303
  "isError": false
251
304
  ])
252
-
253
- case .invalidProductKey:
305
+
306
+ case .authenticationFailed:
254
307
  // Reset initialization state to allow retry
255
308
  self.isInitialized = false
256
309
  sendEvent("onStatusReported", [
257
- "status": "INVALID_PRODUCT_KEY",
310
+ "status": "AUTHENTICATION_FAILED",
258
311
  "value": status.rawValue,
259
312
  "isError": true,
260
- "error": "Invalid product key"
313
+ "error": "Authentication failed"
261
314
  ])
262
-
315
+
263
316
  default:
264
317
  sendEvent("onStatusReported", [
265
- "status": "STATUS: \(status)",
318
+ "status": status.description.uppercased().replacingOccurrences(of: " ", with: "_"),
266
319
  "value": status.rawValue,
267
320
  "isError": true,
268
- "error": "STATUS: \(status)"
321
+ "error": status.description
269
322
  ])
270
323
  // Log error but don't send events to avoid alert dialogs
271
324
  // Handle other scanner errors silently
272
325
  }
273
326
  }
274
327
 
275
- private func mapStatusToString(_ status: LiGTagStatus) -> String {
276
- switch status {
277
- case .ready: return "READY"
278
- case .notDetected: return "NOT_DETECTED"
279
- case .notDecoded: return "NOT_DECODED"
280
- case .invalidPosition: return "INVALID_POSITION"
281
- case .notRegistered: return "NOT_REGISTER"
282
- case .invalidPositionTooClose: return "INVALID_POSITION_TOO_CLOSE"
283
- case .distanceRangeRestrictionNear: return "DISTANCE_RANGE_RESTRICTION_NEAR"
284
- case .distanceRangeRestrictionFar: return "DISTANCE_RANGE_RESTRUCTION_FAR"
285
- case .invalidPositionUnknown: return "INVALID_POSITION_UNKNOWN"
286
- @unknown default: return "UNKNOWN_STATUS"
328
+ private func mapStatusFromBooleans(detected: Bool, decoded: Bool, validPose: Bool, isReady: Bool) -> String {
329
+ if isReady {
330
+ return "READY"
331
+ } else if !detected {
332
+ return "NOT_DETECTED"
333
+ } else if !decoded {
334
+ return "NOT_DECODED"
335
+ } else if !validPose {
336
+ return "INVALID_POSITION"
337
+ } else {
338
+ return "UNKNOWN_STATUS"
287
339
  }
288
340
  }
289
-
290
- private func mapErrorToString(_ error: LiGScannerError) -> String {
291
- switch error {
292
- case .correct: return "TAG_DETECTED"
293
- case .initErrorNotSupport: return "INIT_ERROR_NOT_SUPPORT"
294
- case .permissionLack: return "PERMISSION_LACK"
341
+
342
+ private func mapStatusCodeToString(_ status: StatusCode) -> String {
343
+ switch status {
295
344
  case .notInitialized: return "NOT_INITIALIZED"
296
- case .imageLowQuality: return "IMAGE_LOW_QUALITY"
297
- case .cameraNotAvailable: return "CAMERA_NOT_AVAILABLE"
298
- case .deviceNotSupported: return "DEVICE_NOT_SUPPORTED"
299
- case .cameraAccessException: return "CAMERA_ACCESS_EXCEPTION"
300
- case .internetAnomaly: return "INTERNET_ANOMALY"
301
- case .lackInternetPermission: return "LACK_INTERNET_PERMISSION"
302
- case .lackAccessWifiStatePermission: return "LACK_ACCESS_WIFI_STATE_PERMISSION"
303
- case .lackCameraPermission: return "LACK_CAMERA_PERMISSION"
304
- case .lackExternalStoragePermission: return "LACK_EXTERNAL_STORAGE_PERMISSION"
305
- case .lackReadPhoneStatePermission: return "LACK_READ_PHONE_STATE_PERMISSION"
306
- case .lackAccessNetworkStatePermission: return "LACK_ACCESS_NETWORK_STATE_PERMISSION"
307
- case .serverAnomaly: return "SERVER_ANOMALY"
308
- case .tokenExpired: return "TOKEN_EXPIRED"
309
- case .configurationError: return "CONFIGURATION_ERROR"
345
+ case .noSupportDevice: return "DEVICE_NOT_SUPPORTED"
310
346
  case .deviceIsSupported: return "DEVICE_IS_SUPPORTED"
311
- case .deviceStatusUnknown: return "DEVICE_STATUS_UNKNOWN"
312
347
  case .authenticationOK: return "AUTHENTICATION_OK"
313
348
  case .authenticationFailed: return "AUTHENTICATION_FAILED"
314
349
  case .authenticationTimeout: return "AUTHENTICATION_TIMEOUT"
315
- case .authenticationInterrupted: return "AUTHENTICATION_INTERRUPTED"
316
- case .initializationComplete: return "INITIALIZATION_COMPLETE"
317
- case .alreadyInitialized: return "ALREADY_INITIALIZED"
318
- case .invalidProductKey: return "INVALID_PRODUCT_KEY"
319
- case .cameraPermissionDenied: return "CAMERA_PERMISSION_DENIED"
320
- case .cameraConfigurationFailed: return "CAMERA_CONFIGURATION_FAILED"
321
- case .processingFailed: return "PROCESSING_FAILED"
322
- case .insufficientLighting: return "INSUFFICIENT_LIGHTING"
323
- case .cameraFocusFailed: return "CAMERA_FOCUS_FAILED"
324
- case .networkError: return "NETWORK_ERROR"
325
- case .invalidServerResponse: return "INVALID_SERVER_RESPONSE"
326
- case .invalidConfiguration: return "INVALID_CONFIGURATION"
327
- case .encryptionFailed: return "ENCRYPTION_FAILED"
328
- case .decryptionFailed: return "DECRYPTION_FAILED"
329
350
  case .scannerStopped: return "SCANNER_STOPPED"
330
- case .invalidParameter: return "INVALID_PARAMETER"
331
- case .algorithmError: return "ALGORITHM_ERROR"
332
- case .internalError: return "INTERNAL_ERROR"
333
- case .unknown: return "UNKNOWN"
334
- @unknown default: return "UNKNOWN_STATUS_\(error.rawValue)"
351
+ case .serverAnomaly: return "SERVER_ANOMALY"
352
+ @unknown default: return status.description.uppercased().replacingOccurrences(of: " ", with: "_")
335
353
  }
336
354
  }
337
355
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lig-scanner-expo-sdk",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "LiG Scanner SDK Wrapper for Expo framework library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "lig-scanner-expo-sdk",
24
24
  "LigScannerExpoSdk"
25
25
  ],
26
- "repository": "https://github.com/plainwuatlig/lig-scanner-expo-sdk",
26
+ "repository": "git+https://github.com/plainwuatlig/lig-scanner-expo-sdk.git",
27
27
  "bugs": {
28
28
  "url": "https://github.com/plainwuatlig/lig-scanner-expo-sdk/issues"
29
29
  },
@@ -46,4 +46,4 @@
46
46
  "react": "*",
47
47
  "react-native": "*"
48
48
  }
49
- }
49
+ }