@blife/rn-step-counter 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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.kr.md +159 -0
  3. package/README.md +158 -0
  4. package/StepCounter.podspec +22 -0
  5. package/android/README.md +65 -0
  6. package/android/build.gradle +66 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +15 -0
  9. package/android/src/main/java/com/stepcounter/StepCounterModule.kt +190 -0
  10. package/android/src/main/java/com/stepcounter/StepCounterPackage.kt +53 -0
  11. package/android/src/main/java/com/stepcounter/services/AccelerometerService.kt +147 -0
  12. package/android/src/main/java/com/stepcounter/services/SensorListenService.kt +305 -0
  13. package/android/src/main/java/com/stepcounter/services/StepCounterService.kt +79 -0
  14. package/android/src/main/java/com/stepcounter/utils/AndroidVersionHelper.kt +33 -0
  15. package/android/src/main/java/com/stepcounter/utils/SensorFusionMath.kt +104 -0
  16. package/ios/SOMotionDetecter.h +44 -0
  17. package/ios/SOMotionDetecter.m +91 -0
  18. package/ios/StepCounter.h +7 -0
  19. package/ios/StepCounter.mm +264 -0
  20. package/lib/module/NativeStepCounter.js +21 -0
  21. package/lib/module/NativeStepCounter.js.map +1 -0
  22. package/lib/module/index.js +194 -0
  23. package/lib/module/index.js.map +1 -0
  24. package/lib/module/package.json +1 -0
  25. package/lib/typescript/jest.setup.d.ts +2 -0
  26. package/lib/typescript/jest.setup.d.ts.map +1 -0
  27. package/lib/typescript/package.json +1 -0
  28. package/lib/typescript/src/NativeStepCounter.d.ts +60 -0
  29. package/lib/typescript/src/NativeStepCounter.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +80 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/package.json +138 -0
  33. package/src/NativeStepCounter.ts +62 -0
  34. package/src/index.tsx +263 -0
@@ -0,0 +1,79 @@
1
+ package com.stepcounter.services
2
+
3
+ import android.hardware.Sensor
4
+ import android.hardware.SensorManager
5
+ import com.stepcounter.StepCounterModule
6
+ import java.util.concurrent.TimeUnit
7
+
8
+ /**
9
+ * This class is responsible for listening to the step counter sensor.
10
+ * It is used to count the steps of the user.
11
+ * @param counterModule The module that is responsible for the communication with the react-native layer
12
+ * @param sensorManager The sensor manager that is responsible for the sensor
13
+ * @property sensorType The type of the sensor always Sensor.TYPE_STEP_COUNTER
14
+ * @property sensorTypeString The type of the sensor as a string. so always "Step Counter"
15
+ * @property sensorDelay The integer enum value of delay of the sensor.
16
+ * choose between SensorManager.SENSOR_DELAY_NORMAL or SensorManager.SENSOR_DELAY_UI
17
+ * @property detectedSensor The sensor that is detected
18
+ * @property previousSteps The initial steps or the previous steps.
19
+ * step counter sensor is recording since the last reboot.
20
+ * so if previous step is null, we need to initialize the previous steps with the current steps minus 1.
21
+ * @property currentSteps The current steps
22
+ * @constructor Creates a new StepCounterService
23
+ * @see SensorListenService
24
+ * @see Sensor
25
+ * @see SensorManager
26
+ * @see StepCounterModule
27
+ * @see TimeUnit
28
+ * @see SensorManager.SENSOR_DELAY_NORMAL
29
+ * @see Sensor.TYPE_STEP_COUNTER
30
+ * @see SensorManager.getDefaultSensor {@link SensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)}
31
+ * @see SensorListenService.updateCurrentSteps
32
+ * @see TimeUnit.toMillis
33
+ */
34
+ class StepCounterService(
35
+ counterModule: StepCounterModule,
36
+ sensorManager: SensorManager,
37
+ ) : SensorListenService(counterModule, sensorManager) {
38
+ override val sensorTypeString = "Step Counter"
39
+ override val sensorType = Sensor.TYPE_STEP_COUNTER
40
+ override val detectedSensor: Sensor? = sensorManager.getDefaultSensor(sensorType)
41
+ private var previousSteps: Double = 0.0
42
+ set(value) {
43
+ if (field < value) {
44
+ field = value
45
+ }
46
+ }
47
+ override var currentSteps: Double = 0.0
48
+ set(value) {
49
+ if (field < value) {
50
+ field = value
51
+ }
52
+ }
53
+
54
+ override fun resetSessionState() {
55
+ previousSteps = 0.0
56
+ currentSteps = 0.0
57
+ }
58
+
59
+ /**
60
+ * This function is responsible for updating the current steps.
61
+ * @param [eventData][FloatArray(1) values][android.hardware.SensorEvent.values] The step counter event data
62
+ * @return The current steps
63
+ * @see android.hardware.SensorEvent
64
+ * @see android.hardware.SensorEvent.values
65
+ * @see android.hardware.SensorEvent.timestamp
66
+ */
67
+ override fun updateCurrentSteps(eventData: FloatArray): Boolean {
68
+ // if the time difference is greater than the delay, set the current steps to the step count minus the initial steps
69
+ // if the previous steps aren't initialized yet,
70
+ return if (previousSteps == 0.0) {
71
+ previousSteps = eventData[0].toDouble()
72
+ false
73
+ } else {
74
+ currentSteps = eventData[0].toDouble().minus(previousSteps)
75
+ // set the last update to the current time
76
+ true
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,33 @@
1
+ package com.stepcounter.utils
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager.FEATURE_SENSOR_ACCELEROMETER
5
+ import android.content.pm.PackageManager.FEATURE_SENSOR_STEP_COUNTER
6
+ import android.util.Log
7
+ import androidx.annotation.RequiresFeature
8
+ import com.stepcounter.utils.AndroidVersionHelper.TAG_NAME
9
+ import com.stepcounter.utils.AndroidVersionHelper.isHardwareAccelerometerEnabled
10
+ import com.stepcounter.utils.AndroidVersionHelper.isHardwareStepCounterEnabled
11
+
12
+ /**
13
+ * This class is responsible for the version check of the android device.
14
+ * It is used to check whether the device supports hardware step detection.
15
+ * @property TAG_NAME The name of the class
16
+ * @property isHardwareStepCounterEnabled Decides whether the hardware step counter should be used
17
+ * @property isHardwareAccelerometerEnabled Decides whether the hardware accelerometer should be used
18
+ */
19
+ object AndroidVersionHelper {
20
+ private val TAG_NAME: String = AndroidVersionHelper::class.java.name
21
+
22
+ @RequiresFeature(name = FEATURE_SENSOR_STEP_COUNTER, enforcement = "1")
23
+ fun isHardwareStepCounterEnabled(context: Context): Boolean {
24
+ Log.d(TAG_NAME, "isHardwareStepCounterEnabled: $FEATURE_SENSOR_STEP_COUNTER")
25
+ return context.packageManager.hasSystemFeature(FEATURE_SENSOR_STEP_COUNTER)
26
+ }
27
+
28
+ @RequiresFeature(name = FEATURE_SENSOR_ACCELEROMETER, enforcement = "1")
29
+ fun isHardwareAccelerometerEnabled(context: Context): Boolean {
30
+ Log.d(TAG_NAME, "isHardwareStepCounterEnabled: $FEATURE_SENSOR_ACCELEROMETER")
31
+ return context.packageManager.hasSystemFeature(FEATURE_SENSOR_ACCELEROMETER)
32
+ }
33
+ }
@@ -0,0 +1,104 @@
1
+ package com.stepcounter.utils
2
+
3
+ import com.stepcounter.utils.SensorFusionMath.dot
4
+ import com.stepcounter.utils.SensorFusionMath.norm
5
+ import com.stepcounter.utils.SensorFusionMath.normalize
6
+ import kotlin.math.sqrt
7
+
8
+ /**
9
+ * This class contains all the math functions used in the sensor fusion.
10
+ * @property SensorFusionMath.sum
11
+ * @property SensorFusionMath.cross
12
+ * @property SensorFusionMath.norm
13
+ * @property SensorFusionMath.normalize
14
+ * @property SensorFusionMath.dot
15
+ * @see <a href="https://en.wikipedia.org/wiki/Euclidean_vector">Euclidean vector</a>
16
+ */
17
+ object SensorFusionMath {
18
+ /**
19
+ * The summation function, also known as a sigma function, is commonly used in mathematics to denote
20
+ * the summation of a set of values. In the context of Euclidean vectors, the summation function can
21
+ * be used to calculate the length or magnitude of a vector.
22
+ * The Euclidean [norm], also known as the L2 [norm], of a vector `x = [x1, x2, ..., xn]` is defined as the
23
+ * square root of the sum of the squares of its components.
24
+ * @param vector The array to be calculated.
25
+ * @returns The sum of the array.
26
+ * @see <a href="https://en.wikipedia.org/wiki/Summation">Summation</a>
27
+ */
28
+ fun sum(vector: FloatArray): Float {
29
+ var summation = 0f
30
+ for (v in vector) {
31
+ summation += v
32
+ }
33
+ return summation
34
+ }
35
+
36
+ /**
37
+ * The cross product is a binary operation on two vectors in three-dimensional space that
38
+ * results in a perpendicular vector to the input vectors. It's used in math, physics, engineering,
39
+ * and computer programming and should not be confused with the [dot] product,
40
+ * which is a scalar product that measures the projection of one vector onto another.
41
+ * @param vectorA The first array.
42
+ * @param vectorB The second array.
43
+ * @see <a href="https://en.wikipedia.org/wiki/Cross_product">Cross product</a>
44
+ */
45
+ fun cross(
46
+ vectorA: FloatArray,
47
+ vectorB: FloatArray,
48
+ ): FloatArray {
49
+ val outVector = FloatArray(3)
50
+ outVector[0] = vectorA[1] * vectorB[2] - vectorA[2] * vectorB[1]
51
+ outVector[1] = vectorA[2] * vectorB[0] - vectorA[0] * vectorB[2]
52
+ outVector[2] = vectorA[0] * vectorB[1] - vectorA[1] * vectorB[0]
53
+ return outVector
54
+ }
55
+
56
+ /**
57
+ * In mathematics, a norm is a function from a real or complex [vector] space
58
+ * to the non-negative real numbers that behaves in certain ways like the distance
59
+ * from the origin: it commutes with scaling, obeys a form of the triangle inequality,
60
+ * and is zero only at the origin.
61
+ * @param vector The vector to be calculated.
62
+ * @returns The norm of the vector.
63
+ * @see <a href="https://en.wikipedia.org/wiki/Norm_(mathematics)#Euclidean_norm">Euclidean Normalization</a>
64
+ */
65
+ fun norm(vector: FloatArray): Float {
66
+ var initFloat = 0f
67
+ for (v in vector) {
68
+ initFloat += v * v
69
+ }
70
+ return sqrt(initFloat)
71
+ }
72
+
73
+ /**
74
+ * In mathematics, the dot product or scalar product is an algebraic operation
75
+ * that takes two equal-length sequences of numbers (usually coordinate vectors),
76
+ * and returns a single number.
77
+ * In Euclidean geometry, the dot product of the Cartesian coordinates of two vectors is widely used.
78
+ * @param a The first sequence of numbers.
79
+ * @param b The second sequence of numbers. Must be the same length as the first sequence.
80
+ * @returns The dot product(single number) of the two sequences of numbers.
81
+ * @see <a href="https://en.wikipedia.org/wiki/Dot_product">Dot product</a>
82
+ */
83
+ fun dot(
84
+ a: FloatArray,
85
+ b: FloatArray,
86
+ ): Float = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
87
+
88
+ /**
89
+ * The norm function is a utility function used here to calculate the Euclidean norm of the input vector.
90
+ * The calculated norm value is used to normalize the input [vector] by
91
+ * dividing each component of the input vector by the norm value.
92
+ * Therefore, while both functions use the same mathematical formula for Euclidean vector normalization,
93
+ * they serve different purposes.
94
+ * The [norm] function calculates the norm of a vector, while the [normalize] function returns a new normalized vector.
95
+ * @param vector The input array to be normalized.
96
+ * @return The new normalized vector.
97
+ * @see SensorFusionMath.norm
98
+ */
99
+ fun normalize(vector: FloatArray): FloatArray {
100
+ val normed = norm(vector)
101
+ if (normed == 0.0f) return vector.copyOf()
102
+ return FloatArray(vector.size) { i -> vector[i] / normed }
103
+ }
104
+ }
@@ -0,0 +1,44 @@
1
+ // SOMotionDetecter.h
2
+ // MotionDetection
3
+ //
4
+ // The MIT License (MIT)
5
+ //
6
+ // Created by : Artur Mkrtchyan
7
+ // Copyright (c) 2014 SocialObjects Software. All rights reserved.
8
+ //
9
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ // of this software and associated documentation files (the "Software"), to deal
11
+ // in the Software without restriction, including without limitation the rights
12
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ // copies of the Software, and to permit persons to whom the Software is
14
+ // furnished to do so, subject to the following conditions:
15
+ //
16
+ // The above copyright notice and this permission notice shall be included in
17
+ // all copies or substantial portions of the Software.
18
+ //
19
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ // SOFTWARE
26
+
27
+ #import <Foundation/Foundation.h>
28
+
29
+ @interface SOMotionDetecter : NSObject
30
+
31
+ + (instancetype)sharedInstance;
32
+
33
+ /**
34
+ * Start accelerometer updates.
35
+ * @param callback Will be called every time when new step is detected
36
+ */
37
+ - (void)startDetectionWithUpdateBlock:(void (^)(NSError *error))callback;
38
+
39
+ /**
40
+ * Stop motion manager accelerometer updates
41
+ */
42
+ - (void)stopDetection;
43
+
44
+ @end
@@ -0,0 +1,91 @@
1
+ // SOMotionDetecter.m
2
+ // MotionDetection
3
+ //
4
+ // Created by Artur on 5/15/15.
5
+ // Copyright (c) 2015 Artur Mkrtchyan. All rights reserved.
6
+ //
7
+
8
+ #import "SOMotionDetecter.h"
9
+ #import <CoreMotion/CoreMotion.h>
10
+ #import <UIKit/UIKit.h>
11
+
12
+ #define kUpdateInterval 0.2f
13
+ @interface SOMotionDetecter ()
14
+
15
+ @property(strong, nonatomic) CMMotionManager *motionManager;
16
+ @property(strong, nonatomic) NSOperationQueue *queue;
17
+
18
+ @end
19
+
20
+ @implementation SOMotionDetecter
21
+
22
+ + (instancetype)sharedInstance {
23
+ static id instance;
24
+ static dispatch_once_t onceToken;
25
+ dispatch_once(&onceToken, ^{
26
+ instance = [self new];
27
+ });
28
+
29
+ return instance;
30
+ }
31
+
32
+ - (instancetype)init {
33
+ self = [super init];
34
+
35
+ self.motionManager = [CMMotionManager new];
36
+ self.motionManager.accelerometerUpdateInterval = kUpdateInterval;
37
+ self.motionManager.deviceMotionUpdateInterval = kUpdateInterval;
38
+ self.motionManager.gyroUpdateInterval = kUpdateInterval;
39
+ self.motionManager.magnetometerUpdateInterval = kUpdateInterval;
40
+ self.motionManager.showsDeviceMovementDisplay = YES;
41
+ self.queue = [NSOperationQueue new];
42
+ self.queue.maxConcurrentOperationCount = 1;
43
+
44
+ return self;
45
+ }
46
+
47
+ - (void)startDetectionWithUpdateBlock:(void (^)(NSError *))callback {
48
+ if (self.motionManager.isAccelerometerActive) {
49
+ return;
50
+ }
51
+
52
+ [self.motionManager
53
+ startAccelerometerUpdatesToQueue:self.queue
54
+ withHandler:^(CMAccelerometerData *accelerometerData,
55
+ NSError *error) {
56
+ if (error) {
57
+ if (callback) {
58
+ dispatch_async(dispatch_get_main_queue(), ^{
59
+ callback(error);
60
+ });
61
+ }
62
+ return;
63
+ }
64
+
65
+ CMAcceleration acceleration =
66
+ accelerometerData.acceleration;
67
+
68
+ CGFloat strength = 1.2f;
69
+ BOOL isStep = NO;
70
+ if (fabs(acceleration.x) > strength ||
71
+ fabs(acceleration.y) > strength ||
72
+ fabs(acceleration.z) > strength) {
73
+ isStep = YES;
74
+ }
75
+ if (isStep) {
76
+ if (callback) {
77
+ dispatch_async(dispatch_get_main_queue(), ^{
78
+ callback(nil);
79
+ });
80
+ }
81
+ }
82
+ }];
83
+ }
84
+
85
+ - (void)stopDetection {
86
+ if (self.motionManager.isAccelerometerActive) {
87
+ [self.motionManager stopAccelerometerUpdates];
88
+ }
89
+ }
90
+
91
+ @end
@@ -0,0 +1,7 @@
1
+ #import <StepCounterSpec/StepCounterSpec.h>
2
+ #import <React/RCTEventEmitter.h>
3
+ #import "SOMotionDetecter.h"
4
+
5
+ @interface StepCounter : RCTEventEmitter <NativeStepCounterSpec>
6
+
7
+ @end
@@ -0,0 +1,264 @@
1
+ #import <CoreMotion/CoreMotion.h>
2
+ #import <React/RCTBridge.h>
3
+ #import <React/RCTEventDispatcher.h>
4
+ #import "StepCounter.h"
5
+ #import "SOMotionDetecter.h"
6
+
7
+ @interface StepCounter ()
8
+ @property (nonatomic, readonly) CMPedometer *pedometer;
9
+ @property (nonatomic, strong) dispatch_queue_t stateQueue;
10
+
11
+ // Session control
12
+ @property (nonatomic, strong) NSDate *sessionStartDate;
13
+
14
+ // Baseline + monotonic
15
+ @property (nonatomic) NSInteger baselineSteps; // steps recorded at session start
16
+ @property (nonatomic) NSInteger lastEmittedSteps; // last emitted delta (monotonic)
17
+ @property (nonatomic) BOOL baselineReady;
18
+ @end
19
+
20
+ @implementation StepCounter
21
+
22
+ + (BOOL)requiresMainQueueSetup {
23
+ return YES;
24
+ }
25
+
26
+ @synthesize bridge = _bridge;
27
+ @synthesize callableJSModules = _callableJSModules;
28
+
29
+ RCT_EXPORT_MODULE();
30
+
31
+ - (NSArray<NSString *> *)supportedEvents {
32
+ return @[
33
+ @"StepCounter.stepCounterUpdate",
34
+ @"StepCounter.stepDetected",
35
+ @"StepCounter.errorOccurred",
36
+ @"StepCounter.stepsSensorInfo"
37
+ ];
38
+ }
39
+
40
+ #ifdef RCT_NEW_ARCH_ENABLED
41
+ // In New Architecture, RCTEventEmitter.receiveEvent is not a registered callable JS module.
42
+ // In bridgeless mode (RCTHost), _bridge is nil; events must go through callableJSModules.
43
+ // In bridge-based New Architecture, fall back to _bridge.enqueueJSCall via RCTDeviceEventEmitter.
44
+ - (void)sendEventWithName:(NSString *)eventName body:(id)body {
45
+ NSArray *args = body ? @[eventName, body] : @[eventName];
46
+ if (_callableJSModules) {
47
+ [_callableJSModules invokeModule:@"RCTDeviceEventEmitter"
48
+ method:@"emit"
49
+ withArgs:args];
50
+ } else if (_bridge) {
51
+ [_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
52
+ method:@"emit"
53
+ args:args
54
+ completion:nil];
55
+ }
56
+ }
57
+ #endif // RCT_NEW_ARCH_ENABLED
58
+
59
+ #pragma mark - Public API
60
+
61
+ RCT_EXPORT_METHOD(isStepCountingSupported:(RCTPromiseResolveBlock)resolve
62
+ reject:(RCTPromiseRejectBlock)reject) {
63
+ resolve(@{
64
+ @"granted": @([self authorizationStatus]),
65
+ @"supported": @([CMPedometer isStepCountingAvailable]),
66
+ });
67
+
68
+ [self sendEventWithName:@"StepCounter.stepsSensorInfo"
69
+ body:[self dictionaryAboutSensorInfo]];
70
+ }
71
+
72
+ RCT_EXPORT_METHOD(queryStepCounterDataBetweenDates:(NSDate *)startDate
73
+ endDate:(NSDate *)endDate
74
+ handler:(RCTResponseSenderBlock)handler) {
75
+ [self.pedometer queryPedometerDataFromDate:startDate
76
+ toDate:endDate
77
+ withHandler:^(CMPedometerData *pedometerData, NSError *error) {
78
+ handler(@[
79
+ error.description ?: [NSNull null],
80
+ [self dictionaryFromPedometerData:pedometerData]
81
+ ]);
82
+ }];
83
+ }
84
+
85
+ RCT_EXPORT_METHOD(startStepCounterUpdate:(double)from) {
86
+ // Stop any in-progress pedometer session before starting a new one.
87
+ [self.pedometer stopPedometerUpdates];
88
+
89
+ // --- Timestamp guard (ms vs seconds) ---
90
+ // Valid Unix timestamp in seconds is ~1.7–2.0e9 (2024–2033),
91
+ // while milliseconds is ~1.7–2.0e12.
92
+ double fromSeconds = (from > 1e12) ? (from / 1000.0) : from;
93
+
94
+ // Extra sanity: if caller accidentally passes ms-but-small (unlikely) or future timestamps,
95
+ // clamp to "now" to avoid "future date" sessions.
96
+ NSDate *now = [NSDate date];
97
+ NSDate *requested = (fromSeconds > 0) ? [NSDate dateWithTimeIntervalSince1970:fromSeconds] : now;
98
+ if ([requested timeIntervalSinceDate:now] > 60.0) { // > 60s into the future
99
+ requested = now;
100
+ }
101
+ // Reset session state atomically before starting new handlers.
102
+ dispatch_sync(_stateQueue, ^{
103
+ self->_sessionStartDate = requested;
104
+ self->_baselineSteps = 0;
105
+ self->_lastEmittedSteps = 0;
106
+ self->_baselineReady = NO;
107
+ });
108
+
109
+ // 1) Establish baseline at session start (start -> now).
110
+ // This makes "reset/restart" deterministic: emitted steps are delta since sessionStartDate.
111
+ [self.pedometer queryPedometerDataFromDate:_sessionStartDate
112
+ toDate:now
113
+ withHandler:^(CMPedometerData *data, NSError *error) {
114
+ dispatch_sync(self->_stateQueue, ^{
115
+ self->_baselineSteps = error ? 0 : data.numberOfSteps.integerValue;
116
+ self->_baselineReady = YES;
117
+ });
118
+ if (error) {
119
+ [self sendEventWithName:@"StepCounter.errorOccurred" body:error];
120
+ }
121
+ }];
122
+
123
+ // 2) Live updates from sessionStartDate.
124
+ [self.pedometer startPedometerUpdatesFromDate:_sessionStartDate
125
+ withHandler:^(CMPedometerData *data, NSError *error) {
126
+ if (error) {
127
+ [self sendEventWithName:@"StepCounter.errorOccurred" body:error];
128
+ return;
129
+ }
130
+ if (!data) return;
131
+
132
+ __block NSDictionary *body = nil;
133
+ dispatch_sync(self->_stateQueue, ^{
134
+ if (!self->_baselineReady) return;
135
+
136
+ // Filter: accept only updates whose startDate matches the session start.
137
+ // iOS can interleave "segment/window" updates with different startDate values.
138
+ NSTimeInterval startDiff = fabs([data.startDate timeIntervalSinceDate:self->_sessionStartDate]);
139
+ if (startDiff > 1.0) return;
140
+
141
+ NSInteger cumulative = data.numberOfSteps.integerValue;
142
+ NSInteger delta = cumulative - self->_baselineSteps;
143
+
144
+ // If OS performs corrections or timestamps shift, delta can go negative.
145
+ // Clamp + rebase to keep future deltas sane.
146
+ if (delta < 0) {
147
+ delta = 0;
148
+ self->_baselineSteps = cumulative;
149
+ }
150
+
151
+ // Monotonic guard: never emit backwards.
152
+ if (delta <= self->_lastEmittedSteps) return;
153
+ self->_lastEmittedSteps = delta;
154
+
155
+ NSMutableDictionary *mBody = [[self dictionaryFromPedometerData:data] mutableCopy];
156
+ mBody[@"steps"] = @(delta);
157
+ mBody[@"counterType"] = @"CMPedometer";
158
+ body = [mBody copy];
159
+ });
160
+ if (body) {
161
+ [self sendEventWithName:@"StepCounter.stepCounterUpdate" body:body];
162
+ }
163
+ }];
164
+ }
165
+
166
+ RCT_EXPORT_METHOD(stopStepCounterUpdate) {
167
+ [self.pedometer stopPedometerUpdates];
168
+ [[SOMotionDetecter sharedInstance] stopDetection];
169
+
170
+ dispatch_sync(_stateQueue, ^{
171
+ self->_sessionStartDate = nil;
172
+ self->_baselineSteps = 0;
173
+ self->_lastEmittedSteps = 0;
174
+ self->_baselineReady = NO;
175
+ });
176
+ }
177
+
178
+ RCT_EXPORT_METHOD(startStepsDetection) {
179
+ [[SOMotionDetecter sharedInstance]
180
+ startDetectionWithUpdateBlock:^(NSError *error) {
181
+ if (error) {
182
+ [self sendEventWithName:@"StepCounter.errorOccurred" body:error];
183
+ } else {
184
+ [self sendEventWithName:@"StepCounter.stepDetected" body:@true];
185
+ }
186
+ }];
187
+ }
188
+
189
+ #pragma mark - Sensor info / mapping
190
+
191
+ - (NSDictionary *)dictionaryAboutSensorInfo {
192
+ return @{
193
+ @"name": @"CMPedometer",
194
+ @"granted": @([self authorizationStatus]),
195
+ @"stepCounting": @([CMPedometer isStepCountingAvailable]),
196
+ @"pace": @([CMPedometer isPaceAvailable]),
197
+ @"cadence": @([CMPedometer isCadenceAvailable]),
198
+ @"distance": @([CMPedometer isDistanceAvailable]),
199
+ @"floorCounting": @([CMPedometer isFloorCountingAvailable]),
200
+ };
201
+ }
202
+
203
+ - (NSDictionary *)dictionaryFromPedometerData:(CMPedometerData *)data {
204
+ if (!data) {
205
+ return @{
206
+ @"counterType": @"CMPedometer",
207
+ @"startDate": [NSNull null],
208
+ @"endDate": [NSNull null],
209
+ @"steps": [NSNull null],
210
+ @"distance": [NSNull null],
211
+ @"floorsAscended": [NSNull null],
212
+ @"floorsDescended": [NSNull null],
213
+ };
214
+ }
215
+
216
+ NSNumber *startDate = @((long long)(data.startDate.timeIntervalSince1970 * 1000.0));
217
+ NSNumber *endDate = @((long long)(data.endDate.timeIntervalSince1970 * 1000.0));
218
+
219
+ return @{
220
+ @"counterType": @"CMPedometer",
221
+ @"startDate": startDate ?: [NSNull null],
222
+ @"endDate": endDate ?: [NSNull null],
223
+ @"steps": data.numberOfSteps ?: [NSNull null],
224
+ @"distance": data.distance ?: [NSNull null],
225
+ @"floorsAscended": data.floorsAscended ?: [NSNull null],
226
+ @"floorsDescended": data.floorsDescended ?: [NSNull null],
227
+ };
228
+ }
229
+
230
+ - (BOOL)authorizationStatus {
231
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
232
+ #pragma clang diagnostic push
233
+ #pragma clang diagnostic ignored "-Wpartial-availability"
234
+ CMAuthorizationStatus status = [CMPedometer authorizationStatus];
235
+ return status == CMAuthorizationStatusAuthorized;
236
+ #pragma clang diagnostic pop
237
+ #else
238
+ return NO;
239
+ #endif
240
+ }
241
+
242
+ #pragma mark - Init
243
+
244
+ - (instancetype)init {
245
+ self = [super init];
246
+ if (!self) return nil;
247
+
248
+ _pedometer = [[CMPedometer alloc] init];
249
+ _stateQueue = dispatch_queue_create("com.bonnmh.rnstepcounter.state", DISPATCH_QUEUE_SERIAL);
250
+ _baselineSteps = 0;
251
+ _lastEmittedSteps = 0;
252
+ _baselineReady = NO;
253
+
254
+ return self;
255
+ }
256
+
257
+ #ifdef RCT_NEW_ARCH_ENABLED
258
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
259
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
260
+ return std::make_shared<facebook::react::NativeStepCounterSpecJSI>(params);
261
+ }
262
+ #endif // RCT_NEW_ARCH_ENABLED
263
+
264
+ @end
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from "react-native";
4
+
5
+ /**
6
+ * `StepCountData` is an object with four properties: `distance`, `steps`, `startDate`, and `endDate`.
7
+ * StepCountData object - The Object that contains the step count data.
8
+ * counterType - The type of counter used to count the steps.
9
+ * steps - The number of steps taken during the time period.
10
+ * startDate - The start date of the data.
11
+ * endDate - The end date of the data.
12
+ * distance - The distance in meters that the user has walked or run.
13
+ * floorsAscended - number of floors ascended (iOS only)
14
+ * floorsDescended - number of floors descended (iOS only)
15
+ */
16
+
17
+ export const NAME = "StepCounter";
18
+ export const VERSION = "0.3.1";
19
+ export const eventName = "StepCounter.stepCounterUpdate";
20
+ export default TurboModuleRegistry.getEnforcing("StepCounter");
21
+ //# sourceMappingURL=NativeStepCounter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","NAME","VERSION","eventName","getEnforcing"],"sourceRoot":"../../src","sources":["NativeStepCounter.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;;AAIpE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAWA,OAAO,MAAMC,IAAI,GAAG,aAAa;AACjC,OAAO,MAAMC,OAAO,GAAG,OAAO;AAC9B,OAAO,MAAMC,SAAS,GAAG,+BAA+B;AAkCxD,eAAeH,mBAAmB,CAACI,YAAY,CAAO,aAAa,CAAC","ignoreList":[]}