From 3c0c7d82e391b21dbdd0c645c8a1db694d217b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jay=20P=C3=A9clard?= Date: Fri, 10 Jul 2026 09:47:59 +0200 Subject: [PATCH 1/3] feat(ios): migrate to MotionTag iOS SDK 7.0.0 7.0.0 is a pure-Swift, fully @MainActor-isolated module with no Objective-C surface. It ships no MotionTag.h / MotionTagSDK.h, so MotionTagModule.mm's `#import ` no longer resolves; the SDK is reached only through the Swift shim, whose generated header omits the now non-@objc MotionTagDelegate conformance. Because the delegate protocol is @MainActor and refines Sendable, MotionTagDelegateImpl must be main-actor isolated, so every @objc entry point is now called from the main queue. isTrackingActive stops resolving synchronously on the JS thread; JS already awaited a Promise, so the observable contract is unchanged. `MotionTag` is not class-constrained, so the cached `motionTag` existential has to be a `var` for property writes to type-check. The conformer is a reference type, so writes still reach the shared instance. processBackgroundSessionEvents(with:) lost its completion-handler overload. The async form is exposed as the primary shim; the completion-handler shim is kept because ExpoAppDelegate declares only that spelling, in Swift, and Swift synthesises the async spelling only for Objective-C declarations. Both public signatures are unchanged, so host apps need no source change. Note the vendor's prose guide is stale on two points the compiled interface settles: the label is `launchOptions:`, not `launchOption:`, and its `let motionTag: MotionTag` snippet does not compile. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0124GwP2xm3zYhoUiiSKWbeA --- AGENTS.md | 2 +- README.md | 6 ++--- docs/motiontag-integration.md | 6 ++--- ios/MotionTagBootstrap.swift | 34 +++++++++++++++++++------- ios/MotionTagDelegateImpl.swift | 27 +++++++++++++-------- ios/MotionTagModule.mm | 42 +++++++++++++++++++-------------- react-native-motiontag.podspec | 2 +- 7 files changed, 75 insertions(+), 44 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7b8c9d2..19b59c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ before making changes — most of it is not derivable from the code alone. ## What this repo is A React Native Turbo Module (`@panter/react-native-motiontag`) wrapping -the MotionTag tracking SDKs (iOS 6.5.x, Android 7.2.x). New architecture +the MotionTag tracking SDKs (iOS 7.0.x, Android 7.2.x). New architecture only. JS surface mirrors the official Flutter SDK so payloads stay interchangeable. diff --git a/README.md b/README.md index ca7eb7b..ae14402 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Turbo Module wrapping the [MotionTag tracking SDK](https://motion-tag.com/) for React Native (new architecture only). The JS surface mirrors the [official Flutter SDK](https://github.com/MOTIONTAG/motiontag-sdk-flutter) -so payloads are interchangeable. Bridges the iOS SDK v6.5.x and the Android +so payloads are interchangeable. Bridges the iOS SDK v7.0.x and the Android SDK v7.2.x — the platform asymmetry is hidden behind a shared TS contract. ## Contents @@ -214,7 +214,7 @@ practice this is rare and only affects authorization-status changes; the | Platform | Version | Source | | --- | --- | --- | -| iOS | `MotionTagSDK ~> 6.5.0` | CocoaPods trunk (transitive from this pod) | +| iOS | `MotionTagSDK ~> 7.0.0` | CocoaPods trunk (transitive from this pod) | | Android | `de.motiontag:tracker:7.2.5` | `pkgs.dev.azure.com/motiontag/releases` (Maven repo declared in this package) | The two SDKs are intentionally out of sync — aligning them is tracked as @@ -329,7 +329,7 @@ doesn't publish a GitHub release feed. Both `locale=en` and `locale=de` work. | Axis | Pinned in | Currently | | --- | --- | --- | -| iOS SDK | [`react-native-motiontag.podspec`](react-native-motiontag.podspec) | `MotionTagSDK ~> 6.5.0` | +| iOS SDK | [`react-native-motiontag.podspec`](react-native-motiontag.podspec) | `MotionTagSDK ~> 7.0.0` | | Android SDK | [`android/build.gradle`](android/build.gradle) | `de.motiontag:tracker:7.2.5` | | Example Expo SDK | [`example/package.json`](example/package.json) | `expo ~55` | | Library build tooling | [`package.json`](package.json) | `react-native-builder-bob`, `@expo/config-plugins`, `typescript` | diff --git a/docs/motiontag-integration.md b/docs/motiontag-integration.md index b4506c1..7757eb0 100644 --- a/docs/motiontag-integration.md +++ b/docs/motiontag-integration.md @@ -62,7 +62,7 @@ asymmetry between iOS and Android, and the known smells. │ ios/cyclomania-Bridging-Header │ │ MotionTag.Callback singleton │ │ exposes TrackingSDK.h to Swift │ │ │ │ │ │ MotionTagPackage.java │ -│ Pod: MotionTagSDK ~> 6.5.0 │ │ ReactPackage registering the module │ +│ Pod: MotionTagSDK ~> 7.0.0 │ │ ReactPackage registering the module │ │ │ │ Dep: de.motiontag:tracker:7.2.5 │ └──────────────────────────────────┘ └────────────────────────────────────────┘ ``` @@ -79,7 +79,7 @@ the main reason a refactor will need to be careful. | Platform | Dependency | Version | Source | | --- | --- | --- | --- | -| iOS | `MotionTagSDK` | `~> 6.5.0` | CocoaPods (`ios/Podfile:35`) | +| iOS | `MotionTagSDK` | `~> 7.0.0` | CocoaPods (`ios/Podfile:35`) | | Android | `de.motiontag:tracker` | `7.2.5` | Azure DevOps Maven repo (`android/build.gradle:27-29`) | The iOS framework is also referenced as a binary in `ios/Frameworks/` and @@ -649,7 +649,7 @@ before the first `Activity` (`MainApplication.kt:55`). | Concern | iOS | Android | | --- | --- | --- | -| SDK major version | 6 (`MotionTagSDK ~> 6.5.0`) | 7 (`de.motiontag:tracker:7.2.5`) | +| SDK major version | 7 (`MotionTagSDK ~> 7.0.0`) | 7 (`de.motiontag:tracker:7.2.5`) | | Integration style | `MotionTagDelegate` (callback methods on a delegate object) | `MotionTag.Callback` with sealed `Event` hierarchy in `onEvent(event)` | | Module init point | `AppDelegate.didFinishLaunchingWithOptions` | `MainApplication.onCreate` | | Foreground service | not applicable | required `Notification` passed to `MotionTag.initialize` | diff --git a/ios/MotionTagBootstrap.swift b/ios/MotionTagBootstrap.swift index befe498..3ec4f94 100644 --- a/ios/MotionTagBootstrap.swift +++ b/ios/MotionTagBootstrap.swift @@ -7,24 +7,42 @@ import UIKit /// Initialise the MotionTag SDK. Must be called from `application(_:didFinishLaunchingWithOptions:)` /// before React Native starts up — Turbo Modules are instantiated lazily on first JS access /// and cannot run pre-RN init themselves. + @MainActor @objc public static func bootstrap(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { MotionTagCore.sharedInstance.initialize( using: MotionTagDelegateImpl.shared, - launchOption: launchOptions + launchOptions: launchOptions ) } /// Forward background URL session events so the SDK can finish background uploads on - /// cold-launch wake-ups. Call from `application(_:handleEventsForBackgroundURLSession:completionHandler:)` - /// with every identifier, unconditionally — the SDK decides internally which sessions are its own - /// (matches the MotionTag iOS guide and the official Flutter SDK's AppDelegate). + /// cold-launch wake-ups. Suspends until the SDK's pending events have been delivered. + /// + /// Prefer this over the completion-handler form: a host that owns other background URL + /// sessions can `await` each SDK in turn from a single + /// `application(_:handleEventsForBackgroundURLSession:) async` override, and UIKit invokes + /// the underlying completion handler exactly once when that override returns. + @MainActor + public static func processBackgroundSessionEvents(identifier: String) async { + await MotionTagCore.sharedInstance.processBackgroundSessionEvents(with: identifier) + } + + /// Completion-handler form, for hosts that cannot use the `async` override — notably Expo: + /// `ExpoAppDelegate` declares `application(_:handleEventsForBackgroundURLSession:completionHandler:)` + /// in Swift, and Swift only synthesises the `async` spelling for Objective-C declarations. + /// + /// Unlike SDK v6, the SDK no longer receives the handler and can no longer decide whether an + /// identifier is one of its own, so this calls `completionHandler` for *every* identifier. + /// A host that owns other background URL sessions must therefore ensure the handler is + /// invoked exactly once — chain them, don't call it from each. + @MainActor @objc public static func processBackgroundSessionEvents( identifier: String, completionHandler: @escaping () -> Void ) { - MotionTagCore.sharedInstance.processBackgroundSessionEvents( - with: identifier, - completionHandler: completionHandler - ) + Task { @MainActor in + await processBackgroundSessionEvents(identifier: identifier) + completionHandler() + } } } diff --git a/ios/MotionTagDelegateImpl.swift b/ios/MotionTagDelegateImpl.swift index 3d88bc9..6726414 100644 --- a/ios/MotionTagDelegateImpl.swift +++ b/ios/MotionTagDelegateImpl.swift @@ -4,11 +4,18 @@ import MotionTagSDK public typealias MotionTagEventCallback = ([String: Any]) -> Void -@objc public class MotionTagDelegateImpl: NSObject, MotionTagDelegate { +/// `MotionTagDelegate` is `@MainActor` and refines `Sendable`, so this type must be main-actor +/// isolated — a plain `NSObject` holding a mutable `eventCallback` is not `Sendable`. Every +/// `@objc` entry point below must therefore be called from the main queue. +@objc @MainActor public class MotionTagDelegateImpl: NSObject, MotionTagDelegate { @objc public static let shared = MotionTagDelegateImpl() - private let motionTag = MotionTagCore.sharedInstance + /// `var`, not `let`: `MotionTag` is not class-constrained, so the compiler treats a write + /// through the existential as a mutation of the binding. The conforming type is a reference + /// type (`start()`/`stop()` are non-mutating yet change SDK state), so writes still land on + /// the shared instance rather than on a copy. + private var motionTag = MotionTagCore.sharedInstance public var eventCallback: MotionTagEventCallback? private override init() {} @@ -44,14 +51,14 @@ public typealias MotionTagEventCallback = ([String: Any]) -> Void // MARK: - MotionTagDelegate - public func trackingStatusChanged(_ isTracking: Bool) { + public func trackingDidChange(isTracking: Bool) { emit( ["type": isTracking ? "started" : "stopped"], log: "SDK TrackingStatusChanged: \(isTracking)" ) } - public func locationAuthorizationStatusDidChange(_ status: CLAuthorizationStatus, precise: Bool) { + public func locationAuthorizationDidChange(status: CLAuthorizationStatus, isPrecise: Bool) { let statusString: String switch status { case .authorizedAlways: statusString = "granted" @@ -62,16 +69,16 @@ public typealias MotionTagEventCallback = ([String: Any]) -> Void @unknown default: statusString = "denied" } emit( - ["type": "authorization", "status": statusString, "precise": precise], - log: "SDK CLAuthorizationStatus: \(status.rawValue) precise: \(precise)" + ["type": "authorization", "status": statusString, "precise": isPrecise], + log: "SDK CLAuthorizationStatus: \(status.rawValue) precise: \(isPrecise)" ) } - public func motionActivityAuthorized(_ authorized: Bool) { - emitLog("SDK MotionActivityAuthorized: \(authorized)") + public func motionActivityAuthorizationDidChange(isAuthorized: Bool) { + emitLog("SDK MotionActivityAuthorized: \(isAuthorized)") } - public func didTrackLocation(_ location: CLLocation) { + public func didUpdateLocation(_ location: CLLocation) { emit( [ "type": "location", @@ -87,7 +94,7 @@ public typealias MotionTagEventCallback = ([String: Any]) -> Void ) } - public func dataUploadWithTracked(from startDate: Date, to endDate: Date, didCompleteWithError error: Error?) { + public func dataUploadDidComplete(from startDate: Date, to endDate: Date, error: Error?) { let trackedFromMs = startDate.timeIntervalSince1970 * 1000 let trackedToMs = endDate.timeIntervalSince1970 * 1000 diff --git a/ios/MotionTagModule.mm b/ios/MotionTagModule.mm index dccdfc4..c5676e1 100644 --- a/ios/MotionTagModule.mm +++ b/ios/MotionTagModule.mm @@ -1,10 +1,9 @@ #import "MotionTagModule.h" #import -// Import the SDK before the Swift bridge so the bridge's reference to -// MotionTagDelegate (used as a protocol conformance on -// MotionTagDelegateImpl) is satisfied. -#import +// MotionTagSDK ships no Objective-C headers since v7 — it is a pure Swift module. All SDK +// access goes through MotionTagDelegateImpl, whose generated header omits the (non-@objc) +// MotionTagDelegate conformance, so nothing here needs to see the SDK. // Swift generated header: CocoaPods only exposes it via the framework-style // include when the pod is built as a framework (use_frameworks!). In the // default static-library mode it stays in DerivedSources and is only @@ -33,16 +32,20 @@ @implementation MotionTagModule { return @[@"MotionTagEvent"]; } +// MotionTagDelegateImpl is @MainActor-isolated (the SDK's delegate protocol requires it), so +// every call into it below hops to the main queue first. - (void)startObserving { _hasListeners = YES; __weak __typeof(self) weakSelf = self; - [MotionTagDelegateImpl.shared setEventCallback:^(NSDictionary *event) { - __strong __typeof(weakSelf) strongSelf = weakSelf; - if (strongSelf && strongSelf->_hasListeners) { - [strongSelf sendEventWithName:@"MotionTagEvent" body:event]; - } - }]; + dispatch_async(dispatch_get_main_queue(), ^{ + [MotionTagDelegateImpl.shared setEventCallback:^(NSDictionary *event) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf->_hasListeners) { + [strongSelf sendEventWithName:@"MotionTagEvent" body:event]; + } + }]; + }); } - (void)stopObserving @@ -72,20 +75,23 @@ - (void)setUserToken:(NSString *)jwt resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [MotionTagDelegateImpl.shared setUserToken:jwt]; - resolve(nil); + dispatch_async(dispatch_get_main_queue(), ^{ + [MotionTagDelegateImpl.shared setUserToken:jwt]; + resolve(nil); + }); } - (void)getUserToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - // MotionTag iOS SDK v6 does not expose a public getUserToken API. - // Resolve to nil so JS callers can detect "not implemented on this platform". + // Not wired to the SDK yet. Resolve to nil so JS callers can detect "not implemented here". resolve([NSNull null]); } - (void)isTrackingActive:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - resolve(@([MotionTagDelegateImpl.shared isTrackingActive])); + dispatch_async(dispatch_get_main_queue(), ^{ + resolve(@([MotionTagDelegateImpl.shared isTrackingActive])); + }); } - (void)isPowerSaveModeEnabled:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject @@ -102,7 +108,7 @@ - (void)isBatteryOptimizationsEnabled:(RCTPromiseResolveBlock)resolve reject:(RC - (void)getWifiOnlyDataTransfer:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - // Not exposed by the v6 iOS SDK; return false until v7 migration. + // Not wired to the SDK yet. resolve(@NO); } @@ -110,12 +116,12 @@ - (void)setWifiOnlyDataTransfer:(BOOL)wifiOnly resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - reject(@"UNSUPPORTED", @"setWifiOnlyDataTransfer is not supported on iOS (MotionTag SDK v6).", nil); + reject(@"UNSUPPORTED", @"setWifiOnlyDataTransfer is not implemented on iOS.", nil); } - (void)clearData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - reject(@"UNSUPPORTED", @"clearData is not supported on iOS (MotionTag SDK v6).", nil); + reject(@"UNSUPPORTED", @"clearData is not implemented on iOS.", nil); } - (std::shared_ptr)getTurboModule: diff --git a/react-native-motiontag.podspec b/react-native-motiontag.podspec index 4087525..c6c96a4 100644 --- a/react-native-motiontag.podspec +++ b/react-native-motiontag.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.requires_arc = true s.swift_versions = ["5.0"] - s.dependency "MotionTagSDK", "~> 6.5.0" + s.dependency "MotionTagSDK", "~> 7.0.0" install_modules_dependencies(s) end From e1b700e19e287b9203b1bdd045d98a3d991bd17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jay=20P=C3=A9clard?= Date: Fri, 10 Jul 2026 09:49:14 +0200 Subject: [PATCH 2/3] fix(plugin): chain super in the background URL session override The injected override never called super, so ExpoAppDelegate's subscriber fan-out was skipped entirely. That was masked under SDK v6, where the SDK received the completion handler and invoked it only for its own sessions. Since v7 the SDK no longer takes the handler, so our shim invokes it for every identifier. Combined with a restored super call that would fire a background completion handler twice, so the handler is now passed to super instead: MotionTag drains its session first, then Expo's subscribers run and invoke the handler exactly once. Also switch the bare-RN README snippet to the async override, which lets a host that owns other background URL sessions await each in turn and leaves UIKit to invoke the handler once. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0124GwP2xm3zYhoUiiSKWbeA --- README.md | 23 +++++++++++++---------- plugin/withIosAppDelegate.js | 22 +++++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ae14402..4407529 100644 --- a/README.md +++ b/README.md @@ -130,19 +130,22 @@ func application( func application( _ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void -) { - // Forward every identifier unconditionally — the SDK decides internally - // which sessions are its own. If the app uses Firebase, also call its - // handleEvents(forBackgroundURLSession:) here (see the MotionTag iOS guide). - MotionTagBootstrap.processBackgroundSessionEvents( - identifier: identifier, - completionHandler: completionHandler - ) + handleEventsForBackgroundURLSession identifier: String +) async { + // Forward every identifier unconditionally — a foreign identifier is a + // cheap no-op. UIKit invokes the underlying completion handler once this + // method returns. + await MotionTagBootstrap.processBackgroundSessionEvents(identifier: identifier) } ``` +If the app owns **other** background URL sessions (Firebase, downloads), `await` +them from this same method. The completion handler must be invoked exactly once, +and since SDK v7 MotionTag no longer inspects it — so it can no longer tell you +whether a session was its own. A host that instead uses the completion-handler +overload, `processBackgroundSessionEvents(identifier:completionHandler:)`, is +responsible for chaining rather than calling the handler from each SDK. + The host's `Info.plist` must declare: - `NSLocationAlwaysAndWhenInUseUsageDescription`, diff --git a/plugin/withIosAppDelegate.js b/plugin/withIosAppDelegate.js index e61e881..be8a610 100644 --- a/plugin/withIosAppDelegate.js +++ b/plugin/withIosAppDelegate.js @@ -14,18 +14,26 @@ const BOOTSTRAP_CALL = ` // MotionTag SDK must initialise before React Native // Background uploads run in a background URL session. When iOS wakes the // (possibly killed) app to deliver its events, they must reach the SDK. -// Per the MotionTag iOS guide (and the official Flutter SDK's AppDelegate), -// every identifier is forwarded unconditionally — the SDK decides internally -// which sessions are its own and only calls the completion handler for those. +// Every identifier is forwarded unconditionally — since SDK v7 the SDK no +// longer inspects the handler, so it can no longer tell us whether a session +// is its own; forwarding a foreign identifier is a cheap no-op. +// +// The completion handler is passed to `super`, not called here: ExpoAppDelegate +// fans the event out to its subscribers and invokes the handler exactly once +// (immediately, when there are none). Calling it here as well would invoke a +// background completion handler twice and skip every Expo subscriber. const BACKGROUND_SESSION_OVERRIDE = ` public override func application( _ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void ) { - MotionTagBootstrap.processBackgroundSessionEvents( - identifier: identifier, - completionHandler: completionHandler - ) + MotionTagBootstrap.processBackgroundSessionEvents(identifier: identifier) { + super.application( + application, + handleEventsForBackgroundURLSession: identifier, + completionHandler: completionHandler + ) + } }` /** From 3328f71a1b9122b1ea45409f099e0c10d7497768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jay=20P=C3=A9clard?= Date: Fri, 10 Jul 2026 10:02:00 +0200 Subject: [PATCH 3/3] feat(ios): implement getUserToken, wifiOnlyDataTransfer and clearData These were stubbed out as "not supported on iOS (MotionTag SDK v6)", but v6.5.0 already exposed userToken, wifiOnlyDataTransfer and clearData on its MotionTag protocol. The comments were wrong, not the SDK; the 7.0.0 migration only made that visible. getUserToken now resolves the stored JWT instead of null, getWifiOnlyDataTransfer reports the real flag instead of false, and setWifiOnlyDataTransfer / clearData resolve instead of rejecting UNSUPPORTED. clearData's record count is discarded: the TS spec declares Promise and Android resolves null. isPowerSaveModeEnabled and isBatteryOptimizationsEnabled stay Android-only. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0124GwP2xm3zYhoUiiSKWbeA --- README.md | 9 ++++----- ios/MotionTagDelegateImpl.swift | 18 ++++++++++++++++++ ios/MotionTagModule.mm | 23 +++++++++++++++++------ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4407529..4a80a5e 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,10 @@ own `EventSubscription`. The `MotionTagEvent` discriminated union covers `batteryOptimizationsChanged` (Android), and a fall-through `log` channel that carries the diagnostic string format the underlying SDKs emit. -The platform-only methods (`isPowerSaveModeEnabled`, -`isBatteryOptimizationsEnabled` on Android; `getWifiOnlyDataTransfer` / -`setWifiOnlyDataTransfer` / `clearData` on Android) resolve to safe defaults -(`false`) or reject (`'UNSUPPORTED'`) on iOS, matching the Flutter SDK's -behaviour. +`getUserToken`, `getWifiOnlyDataTransfer`, `setWifiOnlyDataTransfer` and +`clearData` work on both platforms. The Android-only methods +(`isPowerSaveModeEnabled`, `isBatteryOptimizationsEnabled`) resolve to `false` +on iOS, matching the Flutter SDK's behaviour. ## Setup with Expo (recommended) diff --git a/ios/MotionTagDelegateImpl.swift b/ios/MotionTagDelegateImpl.swift index 6726414..8b15c3c 100644 --- a/ios/MotionTagDelegateImpl.swift +++ b/ios/MotionTagDelegateImpl.swift @@ -24,6 +24,10 @@ public typealias MotionTagEventCallback = ([String: Any]) -> Void motionTag.userToken = jwt } + @objc public func getUserToken() -> String? { + return motionTag.userToken + } + @objc public func startTracking() { motionTag.start() } @@ -36,6 +40,20 @@ public typealias MotionTagEventCallback = ([String: Any]) -> Void return motionTag.isTrackingActive } + @objc public func getWifiOnlyDataTransfer() -> Bool { + return motionTag.wifiOnlyDataTransfer + } + + @objc public func setWifiOnlyDataTransfer(_ wifiOnly: Bool) { + motionTag.wifiOnlyDataTransfer = wifiOnly + } + + /// Returns the number of cleared records, which the JS contract discards. + @discardableResult + @objc public func clearData() -> Int { + return motionTag.clearData() + } + @objc public func setEventCallback(_ callback: @escaping MotionTagEventCallback) { eventCallback = callback } diff --git a/ios/MotionTagModule.mm b/ios/MotionTagModule.mm index c5676e1..de9a166 100644 --- a/ios/MotionTagModule.mm +++ b/ios/MotionTagModule.mm @@ -83,8 +83,10 @@ - (void)setUserToken:(NSString *)jwt - (void)getUserToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - // Not wired to the SDK yet. Resolve to nil so JS callers can detect "not implemented here". - resolve([NSNull null]); + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *token = [MotionTagDelegateImpl.shared getUserToken]; + resolve(token ?: [NSNull null]); + }); } - (void)isTrackingActive:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject @@ -108,20 +110,29 @@ - (void)isBatteryOptimizationsEnabled:(RCTPromiseResolveBlock)resolve reject:(RC - (void)getWifiOnlyDataTransfer:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - // Not wired to the SDK yet. - resolve(@NO); + dispatch_async(dispatch_get_main_queue(), ^{ + resolve(@([MotionTagDelegateImpl.shared getWifiOnlyDataTransfer])); + }); } - (void)setWifiOnlyDataTransfer:(BOOL)wifiOnly resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - reject(@"UNSUPPORTED", @"setWifiOnlyDataTransfer is not implemented on iOS.", nil); + dispatch_async(dispatch_get_main_queue(), ^{ + [MotionTagDelegateImpl.shared setWifiOnlyDataTransfer:wifiOnly]; + resolve(nil); + }); } - (void)clearData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - reject(@"UNSUPPORTED", @"clearData is not implemented on iOS.", nil); + dispatch_async(dispatch_get_main_queue(), ^{ + // The SDK returns the number of cleared records; the JS contract is Promise, + // and Android resolves null. Discard it rather than diverge. + [MotionTagDelegateImpl.shared clearData]; + resolve(nil); + }); } - (std::shared_ptr)getTurboModule: