expo-pedometer 0.1.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/android/build.gradle +22 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/expo/modules/pedometer/ExpoPedometerModule.kt +169 -0
  6. package/android/src/main/java/expo/modules/pedometer/ExpoPedometerPermissionRationaleActivity.kt +87 -0
  7. package/app.plugin.js +1 -0
  8. package/build/ExpoPedometer.types.d.ts +10 -0
  9. package/build/ExpoPedometer.types.d.ts.map +1 -0
  10. package/build/ExpoPedometer.types.js +7 -0
  11. package/build/ExpoPedometer.types.js.map +1 -0
  12. package/build/ExpoPedometerModule.d.ts +11 -0
  13. package/build/ExpoPedometerModule.d.ts.map +1 -0
  14. package/build/ExpoPedometerModule.js +3 -0
  15. package/build/ExpoPedometerModule.js.map +1 -0
  16. package/build/ExpoPedometerModule.web.d.ts +11 -0
  17. package/build/ExpoPedometerModule.web.d.ts.map +1 -0
  18. package/build/ExpoPedometerModule.web.js +19 -0
  19. package/build/ExpoPedometerModule.web.js.map +1 -0
  20. package/build/index.d.ts +8 -0
  21. package/build/index.d.ts.map +1 -0
  22. package/build/index.js +16 -0
  23. package/build/index.js.map +1 -0
  24. package/build/usePermissions.d.ts +9 -0
  25. package/build/usePermissions.d.ts.map +1 -0
  26. package/build/usePermissions.js +41 -0
  27. package/build/usePermissions.js.map +1 -0
  28. package/expo-module.config.json +9 -0
  29. package/ios/ExpoPedometer.podspec +22 -0
  30. package/ios/ExpoPedometerModule.swift +118 -0
  31. package/package.json +65 -0
  32. package/plugin/build/index.d.ts +42 -0
  33. package/plugin/build/index.d.ts.map +1 -0
  34. package/plugin/build/index.js +152 -0
  35. package/plugin/build/index.js.map +1 -0
  36. package/src/ExpoPedometer.types.ts +10 -0
  37. package/src/ExpoPedometerModule.ts +12 -0
  38. package/src/ExpoPedometerModule.web.ts +24 -0
  39. package/src/index.ts +21 -0
  40. package/src/usePermissions.ts +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # expo-pedometer
2
+
3
+ Expo module for reading today's step count from platform health data.
4
+
5
+ - iOS reads step count with Core Motion `CMPedometer`.
6
+ - Android reads Health Connect `StepsRecord` aggregates.
7
+ - Web is unavailable and reports denied permissions.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install expo-pedometer
13
+ ```
14
+
15
+ Add the config plugin:
16
+
17
+ ```json
18
+ {
19
+ "expo": {
20
+ "plugins": [
21
+ [
22
+ "expo-pedometer",
23
+ {
24
+ "iosMotionPermission": "Allow this app to read your step count from Motion & Fitness.",
25
+ "androidMinSdkVersion": 26,
26
+ "androidHealthConnectRationaleTitle": "Step count access",
27
+ "androidHealthConnectRationaleDescription": "Step count is read from Health Connect to show today's walking progress.",
28
+ "androidHealthConnectPrivacyPolicyUrl": "https://example.com/privacy"
29
+ }
30
+ ]
31
+ ]
32
+ }
33
+ }
34
+ ```
35
+
36
+ The plugin adds:
37
+
38
+ - iOS `NSMotionUsageDescription`.
39
+ - Android `android.permission.health.READ_STEPS`.
40
+ - Android Health Connect package query and permission rationale manifest entries.
41
+ - Android `android.minSdkVersion=26`, required by AndroidX Health Connect.
42
+
43
+ ## API
44
+
45
+ ```ts
46
+ enum PermissionStatus {
47
+ GRANTED = "granted",
48
+ UNDETERMINED = "undetermined",
49
+ DENIED = "denied",
50
+ }
51
+
52
+ type PermissionResponse = {
53
+ status: PermissionStatus;
54
+ canAskAgain: boolean;
55
+ };
56
+
57
+ export const isAvailableAsync: () => Promise<boolean>;
58
+ export const getPermissionsAsync: () => Promise<PermissionResponse>;
59
+ export const requestPermissionsAsync: () => Promise<PermissionResponse>;
60
+ export const getTodayStepCountAsync: () => Promise<number>;
61
+ export const usePermissions: () => [
62
+ isAvailable: boolean | null,
63
+ permission: PermissionResponse | null,
64
+ requestPermission: () => Promise<PermissionResponse>,
65
+ getPermission: () => Promise<PermissionResponse>,
66
+ ];
67
+ ```
68
+
69
+ Example:
70
+
71
+ ```tsx
72
+ import { PermissionStatus, getTodayStepCountAsync, usePermissions } from "expo-pedometer";
73
+ import { Button } from "react-native";
74
+
75
+ export function StepCount() {
76
+ const [isAvailable, permission, requestPermission] = usePermissions();
77
+
78
+ async function requestAndReadSteps() {
79
+ const nextPermission = await requestPermission();
80
+ if (nextPermission.status === PermissionStatus.GRANTED) {
81
+ const steps = await getTodayStepCountAsync();
82
+ console.log(steps);
83
+ }
84
+ }
85
+
86
+ if (isAvailable === false) {
87
+ return null;
88
+ }
89
+
90
+ return (
91
+ <Button
92
+ title={permission?.status ?? "loading"}
93
+ onPress={requestAndReadSteps}
94
+ />
95
+ );
96
+ }
97
+ ```
98
+
99
+ `usePermissions()` fetches availability and the current permission when the component mounts. Calling
100
+ `requestPermission()` or `getPermission()` updates the returned permission state.
101
+
102
+ ## Android Rationale Localization
103
+
104
+ `androidHealthConnectRationaleTitle`, `androidHealthConnectRationaleDescription`, and
105
+ `androidHealthConnectPrivacyPolicyUrl` can be literal strings or Android string resource references:
106
+
107
+ ```json
108
+ [
109
+ "expo-pedometer",
110
+ {
111
+ "androidHealthConnectRationaleTitle": "@string/pedometer_health_title",
112
+ "androidHealthConnectRationaleDescription": "@string/pedometer_health_description",
113
+ "androidHealthConnectPrivacyPolicyUrl": "@string/privacy_policy_url"
114
+ }
115
+ ]
116
+ ```
117
+
118
+ Define localized values in the app's Android resources, such as
119
+ `android/app/src/main/res/values/strings.xml` and
120
+ `android/app/src/main/res/values-ko/strings.xml`.
121
+
122
+ ## Platform Notes
123
+
124
+ `getTodayStepCountAsync()` should be called after `isAvailableAsync()` and a granted permission response.
125
+
126
+ iOS Core Motion step data is device pedometer data, not Apple Health's full aggregate. It may differ from the Health app when Health includes other sources such as Apple Watch.
127
+
128
+ Android Health Connect availability depends on device, OS version, and Health Connect installation state. If Health Connect is unavailable or needs an update, `isAvailableAsync()` returns `false`.
129
+
130
+ Health data permissions can require store privacy declarations and an app privacy policy before release.
@@ -0,0 +1,22 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'expo.modules.pedometer'
7
+ version = '0.1.0'
8
+
9
+ android {
10
+ namespace "expo.modules.pedometer"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName "0.1.0"
14
+ }
15
+ lintOptions {
16
+ abortOnError false
17
+ }
18
+ }
19
+
20
+ dependencies {
21
+ implementation "androidx.health.connect:connect-client:1.1.0"
22
+ }
@@ -0,0 +1 @@
1
+ <manifest />
@@ -0,0 +1,169 @@
1
+ package expo.modules.pedometer
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import androidx.health.connect.client.HealthConnectClient
6
+ import androidx.health.connect.client.PermissionController
7
+ import androidx.health.connect.client.permission.HealthPermission
8
+ import androidx.health.connect.client.records.StepsRecord
9
+ import androidx.health.connect.client.request.AggregateRequest
10
+ import androidx.health.connect.client.time.TimeRangeFilter
11
+ import expo.modules.kotlin.activityresult.AppContextActivityResultContract
12
+ import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher
13
+ import expo.modules.kotlin.exception.CodedException
14
+ import expo.modules.kotlin.exception.Exceptions
15
+ import expo.modules.kotlin.functions.Coroutine
16
+ import expo.modules.kotlin.modules.Module
17
+ import expo.modules.kotlin.modules.ModuleDefinition
18
+ import java.io.Serializable
19
+ import java.time.Instant
20
+ import java.time.LocalDate
21
+ import java.time.ZoneId
22
+
23
+ class ExpoPedometerModule : Module() {
24
+ private lateinit var permissionsLauncher: AppContextActivityResultLauncher<PermissionRequestInput, Set<String>>
25
+
26
+ override fun definition() = ModuleDefinition {
27
+ Name("ExpoPedometer")
28
+
29
+ RegisterActivityContracts {
30
+ permissionsLauncher = registerForActivityResult(HealthConnectPermissionContract())
31
+ }
32
+
33
+ AsyncFunction("isAvailableAsync") {
34
+ isHealthConnectAvailable()
35
+ }
36
+
37
+ AsyncFunction("getPermissionsAsync").Coroutine<Map<String, Any>> {
38
+ getPermissionResponse()
39
+ }
40
+
41
+ AsyncFunction("requestPermissionsAsync").Coroutine<Map<String, Any>> {
42
+ if (!isHealthConnectAvailable()) {
43
+ permissionResponse(PERMISSION_DENIED, false)
44
+ } else if (hasRequestedPermission() && !getGrantedPermissions().contains(READ_STEPS_PERMISSION)) {
45
+ permissionResponse(PERMISSION_DENIED, false)
46
+ } else {
47
+ setHasRequestedPermission()
48
+ val result = permissionsLauncher.launch(PermissionRequestInput(REQUIRED_PERMISSIONS.toList()))
49
+ val isGranted = result.contains(READ_STEPS_PERMISSION)
50
+ permissionResponse(
51
+ if (isGranted) PERMISSION_GRANTED else PERMISSION_DENIED,
52
+ canAskAgain = false
53
+ )
54
+ }
55
+ }
56
+
57
+ AsyncFunction("getTodayStepCountAsync").Coroutine<Long> {
58
+ if (!isHealthConnectAvailable()) {
59
+ throw StepCountUnavailableException()
60
+ }
61
+
62
+ if (!getGrantedPermissions().contains(READ_STEPS_PERMISSION)) {
63
+ throw MissingStepsPermissionException()
64
+ }
65
+
66
+ val now = Instant.now()
67
+ val startOfDay = LocalDate.now(ZoneId.systemDefault())
68
+ .atStartOfDay(ZoneId.systemDefault())
69
+ .toInstant()
70
+ val result = healthConnectClient.aggregate(
71
+ AggregateRequest(
72
+ metrics = setOf(StepsRecord.COUNT_TOTAL),
73
+ timeRangeFilter = TimeRangeFilter.between(startOfDay, now)
74
+ )
75
+ )
76
+
77
+ result[StepsRecord.COUNT_TOTAL] ?: 0L
78
+ }
79
+ }
80
+
81
+ private val context: Context
82
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
83
+
84
+ private val healthConnectClient: HealthConnectClient
85
+ get() = HealthConnectClient.getOrCreate(context)
86
+
87
+ private fun isHealthConnectAvailable(): Boolean {
88
+ return HealthConnectClient.getSdkStatus(context) == HealthConnectClient.SDK_AVAILABLE
89
+ }
90
+
91
+ private suspend fun getPermissionResponse(): Map<String, Any> {
92
+ if (!isHealthConnectAvailable()) {
93
+ return permissionResponse(PERMISSION_DENIED, false)
94
+ }
95
+
96
+ val isGranted = getGrantedPermissions().contains(READ_STEPS_PERMISSION)
97
+ val status = when {
98
+ isGranted -> PERMISSION_GRANTED
99
+ hasRequestedPermission() -> PERMISSION_DENIED
100
+ else -> PERMISSION_UNDETERMINED
101
+ }
102
+ return permissionResponse(status, canAskAgain = status == PERMISSION_UNDETERMINED)
103
+ }
104
+
105
+ private suspend fun getGrantedPermissions(): Set<String> {
106
+ return healthConnectClient.permissionController.getGrantedPermissions()
107
+ }
108
+
109
+ private fun hasRequestedPermission(): Boolean {
110
+ return context
111
+ .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
112
+ .getBoolean(HAS_REQUESTED_PERMISSION_KEY, false)
113
+ }
114
+
115
+ private fun setHasRequestedPermission() {
116
+ context
117
+ .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
118
+ .edit()
119
+ .putBoolean(HAS_REQUESTED_PERMISSION_KEY, true)
120
+ .apply()
121
+ }
122
+
123
+ private fun permissionResponse(status: String, canAskAgain: Boolean): Map<String, Any> {
124
+ return mapOf(
125
+ "status" to status,
126
+ "canAskAgain" to canAskAgain
127
+ )
128
+ }
129
+
130
+ private class HealthConnectPermissionContract :
131
+ AppContextActivityResultContract<PermissionRequestInput, Set<String>> {
132
+ private val contract = PermissionController.createRequestPermissionResultContract()
133
+
134
+ override fun createIntent(context: Context, input: PermissionRequestInput): Intent {
135
+ return contract.createIntent(context, input.permissions.toSet())
136
+ }
137
+
138
+ override fun parseResult(
139
+ input: PermissionRequestInput,
140
+ resultCode: Int,
141
+ intent: Intent?
142
+ ): Set<String> {
143
+ return contract.parseResult(resultCode, intent)
144
+ }
145
+ }
146
+
147
+ private data class PermissionRequestInput(
148
+ val permissions: List<String>
149
+ ) : Serializable
150
+
151
+ companion object {
152
+ private const val PERMISSION_GRANTED = "granted"
153
+ private const val PERMISSION_DENIED = "denied"
154
+ private const val PERMISSION_UNDETERMINED = "undetermined"
155
+ private const val PREFERENCES_NAME = "expo_pedometer"
156
+ private const val HAS_REQUESTED_PERMISSION_KEY = "has_requested_steps_permission"
157
+
158
+ private val READ_STEPS_PERMISSION = HealthPermission.getReadPermission(StepsRecord::class)
159
+ private val REQUIRED_PERMISSIONS = setOf(READ_STEPS_PERMISSION)
160
+ }
161
+ }
162
+
163
+ private class StepCountUnavailableException : CodedException(
164
+ message = "Health Connect step count is not available on this device"
165
+ )
166
+
167
+ private class MissingStepsPermissionException : CodedException(
168
+ message = "Missing Health Connect READ_STEPS permission"
169
+ )
@@ -0,0 +1,87 @@
1
+ package expo.modules.pedometer
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.content.pm.PackageManager
6
+ import android.net.Uri
7
+ import android.os.Bundle
8
+ import android.view.Gravity
9
+ import android.widget.Button
10
+ import android.widget.LinearLayout
11
+ import android.widget.ScrollView
12
+ import android.widget.TextView
13
+
14
+ class ExpoPedometerPermissionRationaleActivity : Activity() {
15
+ override fun onCreate(savedInstanceState: Bundle?) {
16
+ super.onCreate(savedInstanceState)
17
+
18
+ val metadata = packageManager
19
+ .getActivityInfo(componentName, PackageManager.GET_META_DATA)
20
+ .metaData
21
+ val privacyPolicyUrl = readStringMetaData(metadata, META_PRIVACY_POLICY_URL)
22
+
23
+ val content = LinearLayout(this).apply {
24
+ orientation = LinearLayout.VERTICAL
25
+ gravity = Gravity.CENTER_HORIZONTAL
26
+ setPadding(dp(24), dp(32), dp(24), dp(32))
27
+ }
28
+
29
+ content.addView(TextView(this).apply {
30
+ text = readStringMetaData(metadata, META_TITLE, DEFAULT_TITLE)
31
+ textSize = 22f
32
+ })
33
+
34
+ content.addView(TextView(this).apply {
35
+ text = readStringMetaData(metadata, META_DESCRIPTION, DEFAULT_DESCRIPTION)
36
+ textSize = 16f
37
+ setPadding(0, dp(16), 0, 0)
38
+ })
39
+
40
+ if (!privacyPolicyUrl.isNullOrBlank()) {
41
+ content.addView(Button(this).apply {
42
+ text = "Privacy Policy"
43
+ setPadding(0, dp(16), 0, 0)
44
+ setOnClickListener {
45
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(privacyPolicyUrl)))
46
+ }
47
+ })
48
+ }
49
+
50
+ setContentView(ScrollView(this).apply {
51
+ addView(content)
52
+ })
53
+ }
54
+
55
+ private fun dp(value: Int): Int {
56
+ return (value * resources.displayMetrics.density).toInt()
57
+ }
58
+
59
+ private fun readStringMetaData(metadata: Bundle?, key: String, fallback: String? = null): String? {
60
+ return when (val value = metadata?.get(key)) {
61
+ is Int -> if (value != 0) getString(value) else fallback
62
+ is String -> resolveStringReference(value) ?: fallback
63
+ null -> fallback
64
+ else -> value.toString()
65
+ }
66
+ }
67
+
68
+ private fun resolveStringReference(value: String): String? {
69
+ if (!value.startsWith("@string/")) {
70
+ return value
71
+ }
72
+
73
+ val resourceName = value.removePrefix("@string/")
74
+ val resourceId = resources.getIdentifier(resourceName, "string", packageName)
75
+ return if (resourceId != 0) getString(resourceId) else value
76
+ }
77
+
78
+ companion object {
79
+ const val META_TITLE = "expo.modules.pedometer.HEALTH_PERMISSIONS_RATIONALE_TITLE"
80
+ const val META_DESCRIPTION = "expo.modules.pedometer.HEALTH_PERMISSIONS_RATIONALE_DESCRIPTION"
81
+ const val META_PRIVACY_POLICY_URL = "expo.modules.pedometer.PRIVACY_POLICY_URL"
82
+
83
+ private const val DEFAULT_TITLE = "Health permissions"
84
+ private const val DEFAULT_DESCRIPTION =
85
+ "Step count is read from Health Connect to show today's walking progress."
86
+ }
87
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./plugin/build").default;
@@ -0,0 +1,10 @@
1
+ export declare enum PermissionStatus {
2
+ GRANTED = "granted",
3
+ UNDETERMINED = "undetermined",
4
+ DENIED = "denied"
5
+ }
6
+ export type PermissionResponse = {
7
+ status: PermissionStatus;
8
+ canAskAgain: boolean;
9
+ };
10
+ //# sourceMappingURL=ExpoPedometer.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometer.types.d.ts","sourceRoot":"","sources":["../src/ExpoPedometer.types.ts"],"names":[],"mappings":"AAAA,oBAAY,gBAAgB;IAC1B,OAAO,YAAY;IACnB,YAAY,iBAAiB;IAC7B,MAAM,WAAW;CAClB;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export var PermissionStatus;
2
+ (function (PermissionStatus) {
3
+ PermissionStatus["GRANTED"] = "granted";
4
+ PermissionStatus["UNDETERMINED"] = "undetermined";
5
+ PermissionStatus["DENIED"] = "denied";
6
+ })(PermissionStatus || (PermissionStatus = {}));
7
+ //# sourceMappingURL=ExpoPedometer.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometer.types.js","sourceRoot":"","sources":["../src/ExpoPedometer.types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,gBAIX;AAJD,WAAY,gBAAgB;IAC1B,uCAAmB,CAAA;IACnB,iDAA6B,CAAA;IAC7B,qCAAiB,CAAA;AACnB,CAAC,EAJW,gBAAgB,KAAhB,gBAAgB,QAI3B","sourcesContent":["export enum PermissionStatus {\n GRANTED = \"granted\",\n UNDETERMINED = \"undetermined\",\n DENIED = \"denied\",\n}\n\nexport type PermissionResponse = {\n status: PermissionStatus;\n canAskAgain: boolean;\n};\n"]}
@@ -0,0 +1,11 @@
1
+ import { NativeModule } from "expo";
2
+ import type { PermissionResponse } from "./ExpoPedometer.types";
3
+ declare class ExpoPedometerModule extends NativeModule {
4
+ isAvailableAsync(): Promise<boolean>;
5
+ getPermissionsAsync(): Promise<PermissionResponse>;
6
+ requestPermissionsAsync(): Promise<PermissionResponse>;
7
+ getTodayStepCountAsync(): Promise<number>;
8
+ }
9
+ declare const _default: ExpoPedometerModule;
10
+ export default _default;
11
+ //# sourceMappingURL=ExpoPedometerModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometerModule.d.ts","sourceRoot":"","sources":["../src/ExpoPedometerModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,OAAO,mBAAoB,SAAQ,YAAY;IACpD,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IACpC,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IACtD,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAC1C;;AAED,wBAAyE"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from "expo";
2
+ export default requireNativeModule("ExpoPedometer");
3
+ //# sourceMappingURL=ExpoPedometerModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometerModule.js","sourceRoot":"","sources":["../src/ExpoPedometerModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAWzD,eAAe,mBAAmB,CAAsB,eAAe,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nimport type { PermissionResponse } from \"./ExpoPedometer.types\";\n\ndeclare class ExpoPedometerModule extends NativeModule {\n isAvailableAsync(): Promise<boolean>;\n getPermissionsAsync(): Promise<PermissionResponse>;\n requestPermissionsAsync(): Promise<PermissionResponse>;\n getTodayStepCountAsync(): Promise<number>;\n}\n\nexport default requireNativeModule<ExpoPedometerModule>(\"ExpoPedometer\");\n"]}
@@ -0,0 +1,11 @@
1
+ import { NativeModule } from "expo";
2
+ import { type PermissionResponse } from "./ExpoPedometer.types";
3
+ declare class ExpoPedometerModule extends NativeModule {
4
+ isAvailableAsync(): Promise<boolean>;
5
+ getPermissionsAsync(): Promise<PermissionResponse>;
6
+ requestPermissionsAsync(): Promise<PermissionResponse>;
7
+ getTodayStepCountAsync(): Promise<number>;
8
+ }
9
+ declare const _default: typeof ExpoPedometerModule;
10
+ export default _default;
11
+ //# sourceMappingURL=ExpoPedometerModule.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometerModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoPedometerModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,MAAM,CAAC;AAGvD,OAAO,EAAE,KAAK,kBAAkB,EAAoB,MAAM,uBAAuB,CAAC;AAElF,cAAM,mBAAoB,SAAQ,YAAY;IACtC,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIpC,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIlD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAItD,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAGhD;;AAED,wBAAuE"}
@@ -0,0 +1,19 @@
1
+ import { NativeModule, registerWebModule } from "expo";
2
+ import { UnavailabilityError } from "expo-modules-core";
3
+ import { PermissionStatus } from "./ExpoPedometer.types";
4
+ class ExpoPedometerModule extends NativeModule {
5
+ async isAvailableAsync() {
6
+ return false;
7
+ }
8
+ async getPermissionsAsync() {
9
+ return { status: PermissionStatus.DENIED, canAskAgain: false };
10
+ }
11
+ async requestPermissionsAsync() {
12
+ return { status: PermissionStatus.DENIED, canAskAgain: false };
13
+ }
14
+ async getTodayStepCountAsync() {
15
+ throw new UnavailabilityError("ExpoPedometer", "getTodayStepCountAsync");
16
+ }
17
+ }
18
+ export default registerWebModule(ExpoPedometerModule, "ExpoPedometer");
19
+ //# sourceMappingURL=ExpoPedometerModule.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoPedometerModule.web.js","sourceRoot":"","sources":["../src/ExpoPedometerModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAA2B,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAElF,MAAM,mBAAoB,SAAQ,YAAY;IAC5C,KAAK,CAAC,gBAAgB;QACpB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,mBAAmB;QACvB,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,uBAAuB;QAC3B,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,sBAAsB;QAC1B,MAAM,IAAI,mBAAmB,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC;IAC3E,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAC","sourcesContent":["import { NativeModule, registerWebModule } from \"expo\";\nimport { UnavailabilityError } from \"expo-modules-core\";\n\nimport { type PermissionResponse, PermissionStatus } from \"./ExpoPedometer.types\";\n\nclass ExpoPedometerModule extends NativeModule {\n async isAvailableAsync(): Promise<boolean> {\n return false;\n }\n\n async getPermissionsAsync(): Promise<PermissionResponse> {\n return { status: PermissionStatus.DENIED, canAskAgain: false };\n }\n\n async requestPermissionsAsync(): Promise<PermissionResponse> {\n return { status: PermissionStatus.DENIED, canAskAgain: false };\n }\n\n async getTodayStepCountAsync(): Promise<number> {\n throw new UnavailabilityError(\"ExpoPedometer\", \"getTodayStepCountAsync\");\n }\n}\n\nexport default registerWebModule(ExpoPedometerModule, \"ExpoPedometer\");\n"]}
@@ -0,0 +1,8 @@
1
+ import type { PermissionResponse } from "./ExpoPedometer.types";
2
+ export declare function isAvailableAsync(): Promise<boolean>;
3
+ export declare function getPermissionsAsync(): Promise<PermissionResponse>;
4
+ export declare function requestPermissionsAsync(): Promise<PermissionResponse>;
5
+ export declare function getTodayStepCountAsync(): Promise<number>;
6
+ export * from "./ExpoPedometer.types";
7
+ export * from "./usePermissions";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEzD;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAEvE;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9D;AAED,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import ExpoPedometerModule from "./ExpoPedometerModule";
2
+ export async function isAvailableAsync() {
3
+ return await ExpoPedometerModule.isAvailableAsync();
4
+ }
5
+ export async function getPermissionsAsync() {
6
+ return await ExpoPedometerModule.getPermissionsAsync();
7
+ }
8
+ export async function requestPermissionsAsync() {
9
+ return await ExpoPedometerModule.requestPermissionsAsync();
10
+ }
11
+ export async function getTodayStepCountAsync() {
12
+ return await ExpoPedometerModule.getTodayStepCountAsync();
13
+ }
14
+ export * from "./ExpoPedometer.types";
15
+ export * from "./usePermissions";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AAExD,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,MAAM,mBAAmB,CAAC,gBAAgB,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,OAAO,MAAM,mBAAmB,CAAC,mBAAmB,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB;IAC3C,OAAO,MAAM,mBAAmB,CAAC,uBAAuB,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,OAAO,MAAM,mBAAmB,CAAC,sBAAsB,EAAE,CAAC;AAC5D,CAAC;AAED,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC","sourcesContent":["import type { PermissionResponse } from \"./ExpoPedometer.types\";\nimport ExpoPedometerModule from \"./ExpoPedometerModule\";\n\nexport async function isAvailableAsync(): Promise<boolean> {\n return await ExpoPedometerModule.isAvailableAsync();\n}\n\nexport async function getPermissionsAsync(): Promise<PermissionResponse> {\n return await ExpoPedometerModule.getPermissionsAsync();\n}\n\nexport async function requestPermissionsAsync(): Promise<PermissionResponse> {\n return await ExpoPedometerModule.requestPermissionsAsync();\n}\n\nexport async function getTodayStepCountAsync(): Promise<number> {\n return await ExpoPedometerModule.getTodayStepCountAsync();\n}\n\nexport * from \"./ExpoPedometer.types\";\nexport * from \"./usePermissions\";\n"]}
@@ -0,0 +1,9 @@
1
+ import type { PermissionResponse } from "./ExpoPedometer.types";
2
+ export type UsePermissionsResult = [
3
+ isAvailable: boolean | null,
4
+ permission: PermissionResponse | null,
5
+ requestPermission: () => Promise<PermissionResponse>,
6
+ getPermission: () => Promise<PermissionResponse>
7
+ ];
8
+ export declare function usePermissions(): UsePermissionsResult;
9
+ //# sourceMappingURL=usePermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../src/usePermissions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,OAAO,GAAG,IAAI;IAC3B,UAAU,EAAE,kBAAkB,GAAG,IAAI;IACrC,iBAAiB,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC;IACpD,aAAa,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC;CACjD,CAAC;AAEF,wBAAgB,cAAc,IAAI,oBAAoB,CA0CrD"}
@@ -0,0 +1,41 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import ExpoPedometerModule from "./ExpoPedometerModule";
4
+ export function usePermissions() {
5
+ const isMounted = useRef(true);
6
+ const [isAvailable, setIsAvailable] = useState(null);
7
+ const [permission, setPermission] = useState(null);
8
+ const getAvailability = useCallback(async () => {
9
+ const response = await ExpoPedometerModule.isAvailableAsync();
10
+ if (isMounted.current) {
11
+ setIsAvailable(response);
12
+ }
13
+ return response;
14
+ }, []);
15
+ const getPermission = useCallback(async () => {
16
+ const response = await ExpoPedometerModule.getPermissionsAsync();
17
+ if (isMounted.current) {
18
+ setPermission(response);
19
+ }
20
+ return response;
21
+ }, []);
22
+ const requestPermission = useCallback(async () => {
23
+ const response = await ExpoPedometerModule.requestPermissionsAsync();
24
+ if (isMounted.current) {
25
+ setPermission(response);
26
+ }
27
+ return response;
28
+ }, []);
29
+ useEffect(() => {
30
+ void getAvailability();
31
+ void getPermission();
32
+ }, [getAvailability, getPermission]);
33
+ useEffect(() => {
34
+ isMounted.current = true;
35
+ return () => {
36
+ isMounted.current = false;
37
+ };
38
+ }, []);
39
+ return [isAvailable, permission, requestPermission, getPermission];
40
+ }
41
+ //# sourceMappingURL=usePermissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePermissions.js","sourceRoot":"","sources":["../src/usePermissions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AASxD,MAAM,UAAU,cAAc;IAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IACrE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAA4B,IAAI,CAAC,CAAC;IAE9E,MAAM,eAAe,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC7C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,gBAAgB,EAAE,CAAC;QAC9D,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,cAAc,CAAC,QAAQ,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC3C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,mBAAmB,EAAE,CAAC;QACjE,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,iBAAiB,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC/C,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,uBAAuB,EAAE,CAAC;QACrE,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,eAAe,EAAE,CAAC;QACvB,KAAK,aAAa,EAAE,CAAC;IACvB,CAAC,EAAE,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC,CAAC;IAErC,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;QACzB,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,OAAO,GAAG,KAAK,CAAC;QAC5B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,CAAC,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,CAAC,CAAC;AACrE,CAAC","sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PermissionResponse } from \"./ExpoPedometer.types\";\nimport ExpoPedometerModule from \"./ExpoPedometerModule\";\n\nexport type UsePermissionsResult = [\n isAvailable: boolean | null,\n permission: PermissionResponse | null,\n requestPermission: () => Promise<PermissionResponse>,\n getPermission: () => Promise<PermissionResponse>,\n];\n\nexport function usePermissions(): UsePermissionsResult {\n const isMounted = useRef(true);\n const [isAvailable, setIsAvailable] = useState<boolean | null>(null);\n const [permission, setPermission] = useState<PermissionResponse | null>(null);\n\n const getAvailability = useCallback(async () => {\n const response = await ExpoPedometerModule.isAvailableAsync();\n if (isMounted.current) {\n setIsAvailable(response);\n }\n return response;\n }, []);\n\n const getPermission = useCallback(async () => {\n const response = await ExpoPedometerModule.getPermissionsAsync();\n if (isMounted.current) {\n setPermission(response);\n }\n return response;\n }, []);\n\n const requestPermission = useCallback(async () => {\n const response = await ExpoPedometerModule.requestPermissionsAsync();\n if (isMounted.current) {\n setPermission(response);\n }\n return response;\n }, []);\n\n useEffect(() => {\n void getAvailability();\n void getPermission();\n }, [getAvailability, getPermission]);\n\n useEffect(() => {\n isMounted.current = true;\n return () => {\n isMounted.current = false;\n };\n }, []);\n\n return [isAvailable, permission, requestPermission, getPermission];\n}\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["ExpoPedometerModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.pedometer.ExpoPedometerModule"]
8
+ }
9
+ }
@@ -0,0 +1,22 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'ExpoPedometer'
3
+ s.version = '0.1.0'
4
+ s.summary = 'Core Motion step count module for Expo'
5
+ s.description = 'Reads today step count using Core Motion CMPedometer.'
6
+ s.author = 'Hills'
7
+ s.homepage = 'https://github.com/hills-corp/expo-pedometer'
8
+ s.platforms = {
9
+ :ios => '15.1'
10
+ }
11
+ s.source = { git: 'https://github.com/hills-corp/expo-pedometer.git' }
12
+ s.static_framework = true
13
+
14
+ s.dependency 'ExpoModulesCore'
15
+
16
+ # Swift/Objective-C compatibility
17
+ s.pod_target_xcconfig = {
18
+ 'DEFINES_MODULE' => 'YES',
19
+ }
20
+
21
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
22
+ end
@@ -0,0 +1,118 @@
1
+ import ExpoModulesCore
2
+ import CoreMotion
3
+
4
+ private let permissionGranted = "granted"
5
+ private let permissionDenied = "denied"
6
+ private let permissionUndetermined = "undetermined"
7
+
8
+ private final class StepCountUnavailableException: Exception, @unchecked Sendable {
9
+ override var reason: String {
10
+ "Core Motion step count is not available on this device"
11
+ }
12
+ }
13
+
14
+ private final class MissingStepsPermissionException: Exception, @unchecked Sendable {
15
+ override var reason: String {
16
+ "Motion & Fitness permission is required to read step count"
17
+ }
18
+ }
19
+
20
+ private final class PedometerQueryException: GenericException<String>, @unchecked Sendable {
21
+ override var reason: String {
22
+ "Unable to query Core Motion step count: \(param)"
23
+ }
24
+ }
25
+
26
+ public class ExpoPedometerModule: Module {
27
+ private let pedometer = CMPedometer()
28
+
29
+ public func definition() -> ModuleDefinition {
30
+ Name("ExpoPedometer")
31
+
32
+ AsyncFunction("isAvailableAsync") { () -> Bool in
33
+ Self.isStepCountAvailable()
34
+ }
35
+
36
+ AsyncFunction("getPermissionsAsync") { (promise: Promise) in
37
+ promise.resolve(Self.permissionResponseForCurrentStatus())
38
+ }
39
+
40
+ AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
41
+ guard Self.isStepCountAvailable() else {
42
+ promise.resolve(Self.permissionResponse(permissionDenied, canAskAgain: false))
43
+ return
44
+ }
45
+
46
+ guard Self.permissionStatus() == permissionUndetermined else {
47
+ promise.resolve(Self.permissionResponseForCurrentStatus())
48
+ return
49
+ }
50
+
51
+ let now = Date()
52
+ pedometer.queryPedometerData(from: now.addingTimeInterval(-1), to: now) { _, error in
53
+ if let error {
54
+ if Self.permissionStatus() != permissionUndetermined {
55
+ promise.resolve(Self.permissionResponseForCurrentStatus())
56
+ } else {
57
+ promise.reject(PedometerQueryException(error.localizedDescription))
58
+ }
59
+ return
60
+ }
61
+
62
+ promise.resolve(Self.permissionResponseForCurrentStatus())
63
+ }
64
+ }
65
+
66
+ AsyncFunction("getTodayStepCountAsync") { (promise: Promise) in
67
+ guard Self.isStepCountAvailable() else {
68
+ promise.reject(StepCountUnavailableException())
69
+ return
70
+ }
71
+
72
+ guard Self.permissionStatus() == permissionGranted else {
73
+ promise.reject(MissingStepsPermissionException())
74
+ return
75
+ }
76
+
77
+ let now = Date()
78
+ let startOfDay = Calendar.current.startOfDay(for: now)
79
+ pedometer.queryPedometerData(from: startOfDay, to: now) { data, error in
80
+ if let error {
81
+ promise.reject(PedometerQueryException(error.localizedDescription))
82
+ return
83
+ }
84
+
85
+ promise.resolve(data?.numberOfSteps.intValue ?? 0)
86
+ }
87
+ }
88
+ }
89
+
90
+ private static func isStepCountAvailable() -> Bool {
91
+ CMPedometer.isStepCountingAvailable()
92
+ }
93
+
94
+ private static func permissionStatus() -> String {
95
+ switch CMPedometer.authorizationStatus() {
96
+ case .authorized:
97
+ return permissionGranted
98
+ case .notDetermined:
99
+ return permissionUndetermined
100
+ case .denied, .restricted:
101
+ return permissionDenied
102
+ @unknown default:
103
+ return permissionDenied
104
+ }
105
+ }
106
+
107
+ private static func permissionResponseForCurrentStatus() -> [String: Any] {
108
+ let status = permissionStatus()
109
+ return permissionResponse(status, canAskAgain: status == permissionUndetermined)
110
+ }
111
+
112
+ private static func permissionResponse(_ status: String, canAskAgain: Bool) -> [String: Any] {
113
+ [
114
+ "status": status,
115
+ "canAskAgain": canAskAgain
116
+ ]
117
+ }
118
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "expo-pedometer",
3
+ "version": "0.1.0",
4
+ "description": "Core Motion and Health Connect step count module for Expo",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "node internal/module_scripts/build.js",
9
+ "clean": "node internal/module_scripts/clean.js",
10
+ "lint": "biome check .",
11
+ "test": "node internal/module_scripts/test.js",
12
+ "prepare": "node internal/module_scripts/prepare.js",
13
+ "open:ios": "node internal/module_scripts/open-ios.js",
14
+ "open:android": "node internal/module_scripts/open-android.js"
15
+ },
16
+ "files": [
17
+ "android/build.gradle",
18
+ "android/src",
19
+ "app.plugin.js",
20
+ "build",
21
+ "expo-module.config.json",
22
+ "ios",
23
+ "plugin/build",
24
+ "src"
25
+ ],
26
+ "keywords": [
27
+ "react-native",
28
+ "expo",
29
+ "expo-pedometer",
30
+ "ExpoPedometer"
31
+ ],
32
+ "repository": "https://github.com/hills-corp/expo-pedometer",
33
+ "bugs": {
34
+ "url": "https://github.com/hills-corp/expo-pedometer/issues"
35
+ },
36
+ "author": "Injung Chung <injung.chung@gmail.com> (mu29)",
37
+ "license": "MIT",
38
+ "homepage": "https://github.com/hills-corp/expo-pedometer#readme",
39
+ "dependencies": {},
40
+ "devDependencies": {
41
+ "@babel/core": "7.26.0",
42
+ "@biomejs/biome": "2.4.8",
43
+ "@types/jest": "29.2.1",
44
+ "@types/react": "19.2.14",
45
+ "babel-preset-expo": "55.0.8",
46
+ "expo": "55.0.23",
47
+ "expo-modules-core": "55.0.23",
48
+ "jest": "29.7.0",
49
+ "jest-expo": "55.0.9",
50
+ "react-native": "0.83.6",
51
+ "typescript": "5.9.3"
52
+ },
53
+ "jest": {
54
+ "preset": "jest-expo",
55
+ "roots": [
56
+ "<rootDir>/src",
57
+ "<rootDir>/plugin/src"
58
+ ]
59
+ },
60
+ "peerDependencies": {
61
+ "expo": "*",
62
+ "react": "*",
63
+ "react-native": "*"
64
+ }
65
+ }
@@ -0,0 +1,42 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ type ManifestElement = {
3
+ $: Record<string, string>;
4
+ [key: string]: unknown;
5
+ "intent-filter"?: ManifestElement[];
6
+ action?: ManifestElement[];
7
+ category?: ManifestElement[];
8
+ "meta-data"?: ManifestElement[];
9
+ };
10
+ type ManifestApplication = {
11
+ [key: string]: unknown;
12
+ activity?: ManifestElement[];
13
+ "activity-alias"?: ManifestElement[];
14
+ };
15
+ type AndroidManifest = {
16
+ manifest: {
17
+ [key: string]: unknown;
18
+ queries?: {
19
+ package?: ManifestElement[];
20
+ }[];
21
+ "uses-permission"?: ManifestElement[];
22
+ application?: ManifestApplication[];
23
+ };
24
+ };
25
+ export type ExpoPedometerPluginProps = {
26
+ iosMotionPermission?: string;
27
+ androidMinSdkVersion?: number;
28
+ androidHealthConnectRationaleTitle?: string;
29
+ androidHealthConnectRationaleDescription?: string;
30
+ androidHealthConnectPrivacyPolicyUrl?: string;
31
+ androidHealthConnectRationaleActivity?: string;
32
+ };
33
+ export declare const withExpoPedometer: ConfigPlugin<ExpoPedometerPluginProps>;
34
+ export declare function setIosInfoPlist<T extends Record<string, unknown>>(infoPlist: T, props: ExpoPedometerPluginProps): T;
35
+ export declare function setAndroidMinSdkVersion<T extends {
36
+ type: string;
37
+ key?: string;
38
+ value?: string;
39
+ }[]>(gradleProperties: T, minSdkVersion: number): T;
40
+ export declare function setAndroidManifest<T extends AndroidManifest>(androidManifest: T, props: ExpoPedometerPluginProps): T;
41
+ export default withExpoPedometer;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,qBAAqB,CAAC;AAa7B,KAAK,eAAe,GAAG;IACrB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,eAAe,EAAE,CAAC;IACpC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;CACtC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,QAAQ,EAAE;QACR,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QACvB,OAAO,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;SAAE,EAAE,CAAC;QAC5C,iBAAiB,CAAC,EAAE,eAAe,EAAE,CAAC;QACtC,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;KACrC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,wCAAwC,CAAC,EAAE,MAAM,CAAC;IAClD,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C,qCAAqC,CAAC,EAAE,MAAM,CAAC;CAChD,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,wBAAwB,CAoBpE,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,SAAS,EAAE,CAAC,EACZ,KAAK,EAAE,wBAAwB,KAShC;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EAChG,gBAAgB,EAAE,CAAC,EACnB,aAAa,EAAE,MAAM,KAkBtB;AAED,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,eAAe,EAC1D,eAAe,EAAE,CAAC,EAClB,KAAK,EAAE,wBAAwB,KAahC;AA+GD,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withExpoPedometer = void 0;
4
+ exports.setIosInfoPlist = setIosInfoPlist;
5
+ exports.setAndroidMinSdkVersion = setAndroidMinSdkVersion;
6
+ exports.setAndroidManifest = setAndroidManifest;
7
+ const config_plugins_1 = require("expo/config-plugins");
8
+ const READ_STEPS_PERMISSION = "android.permission.health.READ_STEPS";
9
+ const HEALTH_CONNECT_PACKAGE = "com.google.android.apps.healthdata";
10
+ const SHOW_PERMISSIONS_RATIONALE_ACTION = "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE";
11
+ const VIEW_PERMISSION_USAGE_ACTION = "android.intent.action.VIEW_PERMISSION_USAGE";
12
+ const HEALTH_PERMISSIONS_CATEGORY = "android.intent.category.HEALTH_PERMISSIONS";
13
+ const START_VIEW_PERMISSION_USAGE_PERMISSION = "android.permission.START_VIEW_PERMISSION_USAGE";
14
+ const DEFAULT_RATIONALE_ACTIVITY = "expo.modules.pedometer.ExpoPedometerPermissionRationaleActivity";
15
+ const DEFAULT_PERMISSION_USAGE_ALIAS = "ViewPermissionUsageActivity";
16
+ const IOS_MOTION_USAGE_DESCRIPTION = "NSMotionUsageDescription";
17
+ const withExpoPedometer = (config, props = {}) => {
18
+ config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
19
+ config.modResults = setIosInfoPlist(config.modResults, props);
20
+ return config;
21
+ });
22
+ config = (0, config_plugins_1.withGradleProperties)(config, (config) => {
23
+ var _a;
24
+ config.modResults = setAndroidMinSdkVersion(config.modResults, (_a = props.androidMinSdkVersion) !== null && _a !== void 0 ? _a : 26);
25
+ return config;
26
+ });
27
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
28
+ setAndroidManifest(config.modResults, props);
29
+ return config;
30
+ });
31
+ return config;
32
+ };
33
+ exports.withExpoPedometer = withExpoPedometer;
34
+ function setIosInfoPlist(infoPlist, props) {
35
+ var _a;
36
+ const plist = infoPlist;
37
+ if (props.iosMotionPermission || !plist[IOS_MOTION_USAGE_DESCRIPTION]) {
38
+ plist[IOS_MOTION_USAGE_DESCRIPTION] =
39
+ (_a = props.iosMotionPermission) !== null && _a !== void 0 ? _a : "Allow this app to read your step count from Motion & Fitness.";
40
+ }
41
+ return infoPlist;
42
+ }
43
+ function setAndroidMinSdkVersion(gradleProperties, minSdkVersion) {
44
+ var _a;
45
+ const targetValue = String(Math.max(minSdkVersion, 26));
46
+ const existing = gradleProperties.find((property) => property.type === "property" && property.key === "android.minSdkVersion");
47
+ if (existing) {
48
+ existing.value = String(Math.max(Number((_a = existing.value) !== null && _a !== void 0 ? _a : 0), Number(targetValue)));
49
+ return gradleProperties;
50
+ }
51
+ gradleProperties.push({
52
+ type: "property",
53
+ key: "android.minSdkVersion",
54
+ value: targetValue,
55
+ });
56
+ return gradleProperties;
57
+ }
58
+ function setAndroidManifest(androidManifest, props) {
59
+ var _a;
60
+ addNamedElement(androidManifest.manifest, "uses-permission", READ_STEPS_PERMISSION);
61
+ addQueryPackage(androidManifest, HEALTH_CONNECT_PACKAGE);
62
+ const application = getMainApplication(androidManifest);
63
+ const rationaleActivity = (_a = props.androidHealthConnectRationaleActivity) !== null && _a !== void 0 ? _a : DEFAULT_RATIONALE_ACTIVITY;
64
+ addOrUpdateActivity(application, rationaleActivity, props);
65
+ addOrUpdatePermissionUsageAlias(application, rationaleActivity);
66
+ return androidManifest;
67
+ }
68
+ function addQueryPackage(androidManifest, packageName) {
69
+ var _a, _b;
70
+ const manifest = androidManifest.manifest;
71
+ manifest.queries = (_a = manifest.queries) !== null && _a !== void 0 ? _a : [{}];
72
+ const queries = manifest.queries[0];
73
+ queries.package = (_b = queries.package) !== null && _b !== void 0 ? _b : [];
74
+ addNamedElement(queries, "package", packageName);
75
+ }
76
+ function getMainApplication(androidManifest) {
77
+ var _a;
78
+ androidManifest.manifest.application = (_a = androidManifest.manifest.application) !== null && _a !== void 0 ? _a : [{}];
79
+ return androidManifest.manifest.application[0];
80
+ }
81
+ function addOrUpdateActivity(application, activityName, props) {
82
+ var _a, _b, _c, _d, _e;
83
+ application.activity = (_a = application.activity) !== null && _a !== void 0 ? _a : [];
84
+ const activity = (_b = application.activity.find((activity) => activity.$["android:name"] === activityName)) !== null && _b !== void 0 ? _b : addElement(application.activity, activityName);
85
+ activity.$["android:exported"] = "true";
86
+ activity["intent-filter"] = upsertIntentFilter(activity["intent-filter"], SHOW_PERMISSIONS_RATIONALE_ACTION);
87
+ activity["meta-data"] = upsertMetaData(activity["meta-data"], {
88
+ "expo.modules.pedometer.HEALTH_PERMISSIONS_RATIONALE_TITLE": (_c = props.androidHealthConnectRationaleTitle) !== null && _c !== void 0 ? _c : "Health permissions",
89
+ "expo.modules.pedometer.HEALTH_PERMISSIONS_RATIONALE_DESCRIPTION": (_d = props.androidHealthConnectRationaleDescription) !== null && _d !== void 0 ? _d : "Step count is read from Health Connect to show today's walking progress.",
90
+ "expo.modules.pedometer.PRIVACY_POLICY_URL": (_e = props.androidHealthConnectPrivacyPolicyUrl) !== null && _e !== void 0 ? _e : "",
91
+ });
92
+ }
93
+ function addOrUpdatePermissionUsageAlias(application, targetActivity) {
94
+ var _a, _b;
95
+ application["activity-alias"] = (_a = application["activity-alias"]) !== null && _a !== void 0 ? _a : [];
96
+ const alias = (_b = application["activity-alias"].find((alias) => alias.$["android:name"] === DEFAULT_PERMISSION_USAGE_ALIAS)) !== null && _b !== void 0 ? _b : addElement(application["activity-alias"], DEFAULT_PERMISSION_USAGE_ALIAS);
97
+ alias.$["android:exported"] = "true";
98
+ alias.$["android:targetActivity"] = targetActivity;
99
+ alias.$["android:permission"] = START_VIEW_PERMISSION_USAGE_PERMISSION;
100
+ alias["intent-filter"] = [
101
+ {
102
+ $: {},
103
+ action: [{ $: { "android:name": VIEW_PERMISSION_USAGE_ACTION } }],
104
+ category: [{ $: { "android:name": HEALTH_PERMISSIONS_CATEGORY } }],
105
+ },
106
+ ];
107
+ }
108
+ function upsertIntentFilter(intentFilters, actionName) {
109
+ const nextIntentFilters = intentFilters !== null && intentFilters !== void 0 ? intentFilters : [];
110
+ const existing = nextIntentFilters.find((intentFilter) => { var _a; return (_a = intentFilter.action) === null || _a === void 0 ? void 0 : _a.some((action) => action.$["android:name"] === actionName); });
111
+ if (existing) {
112
+ return nextIntentFilters;
113
+ }
114
+ nextIntentFilters.push({
115
+ $: {},
116
+ action: [{ $: { "android:name": actionName } }],
117
+ });
118
+ return nextIntentFilters;
119
+ }
120
+ function upsertMetaData(metadata, values) {
121
+ const nextMetadata = metadata !== null && metadata !== void 0 ? metadata : [];
122
+ for (const [name, value] of Object.entries(values)) {
123
+ const existing = nextMetadata.find((item) => item.$["android:name"] === name);
124
+ if (existing) {
125
+ existing.$["android:value"] = value;
126
+ }
127
+ else {
128
+ nextMetadata.push({
129
+ $: {
130
+ "android:name": name,
131
+ "android:value": value,
132
+ },
133
+ });
134
+ }
135
+ }
136
+ return nextMetadata;
137
+ }
138
+ function addNamedElement(parent, key, name) {
139
+ var _a;
140
+ const elements = (_a = parent[key]) !== null && _a !== void 0 ? _a : [];
141
+ parent[key] = elements;
142
+ if (!elements.some((item) => item.$["android:name"] === name)) {
143
+ addElement(elements, name);
144
+ }
145
+ }
146
+ function addElement(elements, name) {
147
+ const element = { $: { "android:name": name } };
148
+ elements.push(element);
149
+ return element;
150
+ }
151
+ exports.default = exports.withExpoPedometer;
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyEA,0CAWC;AAED,0DAoBC;AAED,gDAeC;AA3HD,wDAK6B;AAE7B,MAAM,qBAAqB,GAAG,sCAAsC,CAAC;AACrE,MAAM,sBAAsB,GAAG,oCAAoC,CAAC;AACpE,MAAM,iCAAiC,GAAG,mDAAmD,CAAC;AAC9F,MAAM,4BAA4B,GAAG,6CAA6C,CAAC;AACnF,MAAM,2BAA2B,GAAG,4CAA4C,CAAC;AACjF,MAAM,sCAAsC,GAAG,gDAAgD,CAAC;AAChG,MAAM,0BAA0B,GAC9B,iEAAiE,CAAC;AACpE,MAAM,8BAA8B,GAAG,6BAA6B,CAAC;AACrE,MAAM,4BAA4B,GAAG,0BAA0B,CAAC;AAmCzD,MAAM,iBAAiB,GAA2C,CAAC,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE;IAC9F,MAAM,GAAG,IAAA,8BAAa,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QACxC,MAAM,CAAC,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,IAAA,qCAAoB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;;QAC/C,MAAM,CAAC,UAAU,GAAG,uBAAuB,CACzC,MAAM,CAAC,UAAU,EACjB,MAAA,KAAK,CAAC,oBAAoB,mCAAI,EAAE,CACjC,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,IAAA,oCAAmB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QAC9C,kBAAkB,CAAC,MAAM,CAAC,UAA6B,EAAE,KAAK,CAAC,CAAC;QAChE,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AApBW,QAAA,iBAAiB,qBAoB5B;AAEF,SAAgB,eAAe,CAC7B,SAAY,EACZ,KAA+B;;IAE/B,MAAM,KAAK,GAAG,SAAoC,CAAC;IACnD,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,4BAA4B,CAAC,EAAE,CAAC;QACtE,KAAK,CAAC,4BAA4B,CAAC;YACjC,MAAA,KAAK,CAAC,mBAAmB,mCAAI,+DAA+D,CAAC;IACjG,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAgB,uBAAuB,CACrC,gBAAmB,EACnB,aAAqB;;IAErB,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CACpC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,UAAU,IAAI,QAAQ,CAAC,GAAG,KAAK,uBAAuB,CACvF,CAAC;IAEF,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAA,QAAQ,CAAC,KAAK,mCAAI,CAAC,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACpF,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,gBAAgB,CAAC,IAAI,CAAC;QACpB,IAAI,EAAE,UAAU;QAChB,GAAG,EAAE,uBAAuB;QAC5B,KAAK,EAAE,WAAW;KACnB,CAAC,CAAC;IACH,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,SAAgB,kBAAkB,CAChC,eAAkB,EAClB,KAA+B;;IAE/B,eAAe,CAAC,eAAe,CAAC,QAAQ,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;IACpF,eAAe,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC;IAEzD,MAAM,WAAW,GAAG,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACxD,MAAM,iBAAiB,GACrB,MAAA,KAAK,CAAC,qCAAqC,mCAAI,0BAA0B,CAAC;IAE5E,mBAAmB,CAAC,WAAW,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC;IAC3D,+BAA+B,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAEhE,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,SAAS,eAAe,CAAC,eAAgC,EAAE,WAAmB;;IAC5E,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC;IAC1C,QAAQ,CAAC,OAAO,GAAG,MAAA,QAAQ,CAAC,OAAO,mCAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACpC,OAAO,CAAC,OAAO,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,EAAE,CAAC;IACxC,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,kBAAkB,CAAC,eAAgC;;IAC1D,eAAe,CAAC,QAAQ,CAAC,WAAW,GAAG,MAAA,eAAe,CAAC,QAAQ,CAAC,WAAW,mCAAI,CAAC,EAAE,CAAC,CAAC;IACpF,OAAO,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,mBAAmB,CAC1B,WAAgC,EAChC,YAAoB,EACpB,KAA+B;;IAE/B,WAAW,CAAC,QAAQ,GAAG,MAAA,WAAW,CAAC,QAAQ,mCAAI,EAAE,CAAC;IAClD,MAAM,QAAQ,GACZ,MAAA,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,YAAY,CAAC,mCACpF,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IAEjD,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,MAAM,CAAC;IACxC,QAAQ,CAAC,eAAe,CAAC,GAAG,kBAAkB,CAC5C,QAAQ,CAAC,eAAe,CAAC,EACzB,iCAAiC,CAClC,CAAC;IACF,QAAQ,CAAC,WAAW,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE;QAC5D,2DAA2D,EACzD,MAAA,KAAK,CAAC,kCAAkC,mCAAI,oBAAoB;QAClE,iEAAiE,EAC/D,MAAA,KAAK,CAAC,wCAAwC,mCAC9C,0EAA0E;QAC5E,2CAA2C,EAAE,MAAA,KAAK,CAAC,oCAAoC,mCAAI,EAAE;KAC9F,CAAC,CAAC;AACL,CAAC;AAED,SAAS,+BAA+B,CAAC,WAAgC,EAAE,cAAsB;;IAC/F,WAAW,CAAC,gBAAgB,CAAC,GAAG,MAAA,WAAW,CAAC,gBAAgB,CAAC,mCAAI,EAAE,CAAC;IACpE,MAAM,KAAK,GACT,MAAA,WAAW,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAChC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,8BAA8B,CACtE,mCAAI,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC,CAAC;IAEjF,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,MAAM,CAAC;IACrC,KAAK,CAAC,CAAC,CAAC,wBAAwB,CAAC,GAAG,cAAc,CAAC;IACnD,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,GAAG,sCAAsC,CAAC;IACvE,KAAK,CAAC,eAAe,CAAC,GAAG;QACvB;YACE,CAAC,EAAE,EAAE;YACL,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,4BAA4B,EAAE,EAAE,CAAC;YACjE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,EAAE,CAAC;SACnE;KACF,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,aAA4C,EAAE,UAAkB;IAC1F,MAAM,iBAAiB,GAAG,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,WACvD,OAAA,MAAA,YAAY,CAAC,MAAM,0CAAE,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,UAAU,CAAC,CAAA,EAAA,CAC/E,CAAC;IAEF,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED,iBAAiB,CAAC,IAAI,CAAC;QACrB,CAAC,EAAE,EAAE;QACL,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,EAAE,CAAC;KAChD,CAAC,CAAC;IACH,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,SAAS,cAAc,CAAC,QAAuC,EAAE,MAA8B;IAC7F,MAAM,YAAY,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,CAAC;QAC9E,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC;gBAChB,CAAC,EAAE;oBACD,cAAc,EAAE,IAAI;oBACpB,eAAe,EAAE,KAAK;iBACvB;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,MAA+B,EAAE,GAAW,EAAE,IAAY;;IACjF,MAAM,QAAQ,GAAG,MAAC,MAAM,CAAC,GAAG,CAAmC,mCAAI,EAAE,CAAC;IACtE,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC;IAEvB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QAC9D,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,QAA2B,EAAE,IAAY;IAC3D,MAAM,OAAO,GAAoB,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,CAAC;IACjE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,kBAAe,yBAAiB,CAAC"}
@@ -0,0 +1,10 @@
1
+ export enum PermissionStatus {
2
+ GRANTED = "granted",
3
+ UNDETERMINED = "undetermined",
4
+ DENIED = "denied",
5
+ }
6
+
7
+ export type PermissionResponse = {
8
+ status: PermissionStatus;
9
+ canAskAgain: boolean;
10
+ };
@@ -0,0 +1,12 @@
1
+ import { NativeModule, requireNativeModule } from "expo";
2
+
3
+ import type { PermissionResponse } from "./ExpoPedometer.types";
4
+
5
+ declare class ExpoPedometerModule extends NativeModule {
6
+ isAvailableAsync(): Promise<boolean>;
7
+ getPermissionsAsync(): Promise<PermissionResponse>;
8
+ requestPermissionsAsync(): Promise<PermissionResponse>;
9
+ getTodayStepCountAsync(): Promise<number>;
10
+ }
11
+
12
+ export default requireNativeModule<ExpoPedometerModule>("ExpoPedometer");
@@ -0,0 +1,24 @@
1
+ import { NativeModule, registerWebModule } from "expo";
2
+ import { UnavailabilityError } from "expo-modules-core";
3
+
4
+ import { type PermissionResponse, PermissionStatus } from "./ExpoPedometer.types";
5
+
6
+ class ExpoPedometerModule extends NativeModule {
7
+ async isAvailableAsync(): Promise<boolean> {
8
+ return false;
9
+ }
10
+
11
+ async getPermissionsAsync(): Promise<PermissionResponse> {
12
+ return { status: PermissionStatus.DENIED, canAskAgain: false };
13
+ }
14
+
15
+ async requestPermissionsAsync(): Promise<PermissionResponse> {
16
+ return { status: PermissionStatus.DENIED, canAskAgain: false };
17
+ }
18
+
19
+ async getTodayStepCountAsync(): Promise<number> {
20
+ throw new UnavailabilityError("ExpoPedometer", "getTodayStepCountAsync");
21
+ }
22
+ }
23
+
24
+ export default registerWebModule(ExpoPedometerModule, "ExpoPedometer");
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { PermissionResponse } from "./ExpoPedometer.types";
2
+ import ExpoPedometerModule from "./ExpoPedometerModule";
3
+
4
+ export async function isAvailableAsync(): Promise<boolean> {
5
+ return await ExpoPedometerModule.isAvailableAsync();
6
+ }
7
+
8
+ export async function getPermissionsAsync(): Promise<PermissionResponse> {
9
+ return await ExpoPedometerModule.getPermissionsAsync();
10
+ }
11
+
12
+ export async function requestPermissionsAsync(): Promise<PermissionResponse> {
13
+ return await ExpoPedometerModule.requestPermissionsAsync();
14
+ }
15
+
16
+ export async function getTodayStepCountAsync(): Promise<number> {
17
+ return await ExpoPedometerModule.getTodayStepCountAsync();
18
+ }
19
+
20
+ export * from "./ExpoPedometer.types";
21
+ export * from "./usePermissions";
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import type { PermissionResponse } from "./ExpoPedometer.types";
5
+ import ExpoPedometerModule from "./ExpoPedometerModule";
6
+
7
+ export type UsePermissionsResult = [
8
+ isAvailable: boolean | null,
9
+ permission: PermissionResponse | null,
10
+ requestPermission: () => Promise<PermissionResponse>,
11
+ getPermission: () => Promise<PermissionResponse>,
12
+ ];
13
+
14
+ export function usePermissions(): UsePermissionsResult {
15
+ const isMounted = useRef(true);
16
+ const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
17
+ const [permission, setPermission] = useState<PermissionResponse | null>(null);
18
+
19
+ const getAvailability = useCallback(async () => {
20
+ const response = await ExpoPedometerModule.isAvailableAsync();
21
+ if (isMounted.current) {
22
+ setIsAvailable(response);
23
+ }
24
+ return response;
25
+ }, []);
26
+
27
+ const getPermission = useCallback(async () => {
28
+ const response = await ExpoPedometerModule.getPermissionsAsync();
29
+ if (isMounted.current) {
30
+ setPermission(response);
31
+ }
32
+ return response;
33
+ }, []);
34
+
35
+ const requestPermission = useCallback(async () => {
36
+ const response = await ExpoPedometerModule.requestPermissionsAsync();
37
+ if (isMounted.current) {
38
+ setPermission(response);
39
+ }
40
+ return response;
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ void getAvailability();
45
+ void getPermission();
46
+ }, [getAvailability, getPermission]);
47
+
48
+ useEffect(() => {
49
+ isMounted.current = true;
50
+ return () => {
51
+ isMounted.current = false;
52
+ };
53
+ }, []);
54
+
55
+ return [isAvailable, permission, requestPermission, getPermission];
56
+ }