expo-beacon 0.7.11 → 0.7.13

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.
@@ -1,9 +1,16 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  xmlns:tools="http://schemas.android.com/tools">
3
3
 
4
- <!-- Bluetooth permissions (Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT) -->
5
- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
6
- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
4
+ <!-- Bluetooth permissions.
5
+ BLUETOOTH and BLUETOOTH_ADMIN are legacy normal permissions required by AltBeacon's
6
+ PermissionsInspector on all API levels. On Android 12+ they are automatically granted
7
+ and act as no-ops, but removing them (via maxSdkVersion="30") causes AltBeacon to log
8
+ errors and believe BLE scanning is unavailable.
9
+ BLUETOOTH_SCAN and BLUETOOTH_CONNECT cover the runtime permission requirements on API 31+. -->
10
+ <uses-permission android:name="android.permission.BLUETOOTH" />
11
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
12
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
13
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
7
14
  <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
8
15
  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
9
16
 
@@ -61,6 +61,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
61
61
  private val notifIdCounter = AtomicInteger(0)
62
62
  private val notifIdMap = java.util.concurrent.ConcurrentHashMap<String, Int>()
63
63
 
64
+ // Debounce guard: prevent loadAndMonitorRegions() from running more than once per 500 ms.
65
+ // This protects against rapid double-invocations when JS calls stopMonitoring/startMonitoring
66
+ // in quick succession during activity lifecycle transitions (e.g. onHostPause → onHostResume),
67
+ // which would otherwise stop and restart all AltBeacon regions within milliseconds.
68
+ @Volatile private var lastLoadRegionsMs: Long = 0L
69
+
64
70
  // Distance logging
65
71
  private val distanceLogRegions = java.util.concurrent.CopyOnWriteArraySet<Region>()
66
72
 
@@ -189,6 +195,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
189
195
  }
190
196
 
191
197
  private fun loadAndMonitorRegions() {
198
+ val now = android.os.SystemClock.elapsedRealtime()
199
+ val elapsed = now - lastLoadRegionsMs
200
+ if (elapsed < LOAD_REGIONS_DEBOUNCE_MS) {
201
+ Log.d(TAG, "loadAndMonitorRegions: skipping duplicate call (${elapsed}ms after last load)")
202
+ return
203
+ }
204
+ lastLoadRegionsMs = now
205
+
192
206
  val prefs: SharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
193
207
  val json = prefs.getString(PREFS_KEY, "[]") ?: "[]"
194
208
  val beacons = try { JSONArray(json) } catch (_: Exception) { JSONArray() }
@@ -799,6 +813,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
799
813
  private const val MAX_STARTFOREGROUND_RETRIES = 3
800
814
  private const val RETRY_DELAY_MS = 10_000L
801
815
  private const val RETRY_SERVICE_REQUEST_CODE = 0x42454143 // "BEAC"
816
+ /** Minimum milliseconds between consecutive loadAndMonitorRegions() calls. */
817
+ private const val LOAD_REGIONS_DEBOUNCE_MS = 500L
802
818
  @Volatile private var activeService: BeaconForegroundService? = null
803
819
 
804
820
  fun start(context: Context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"withBeaconIOS.d.ts","sourceRoot":"","sources":["../src/withBeaconIOS.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAGb,MAAM,sBAAsB,CAAC;AAM9B,eAAO,MAAM,gBAAgB,mvBAkB5B,CAAC;AA0IF,QAAA,MAAM,aAAa,EAAE,YAkCpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"withBeaconIOS.d.ts","sourceRoot":"","sources":["../src/withBeaconIOS.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAGb,MAAM,sBAAsB,CAAC;AAM9B,eAAO,MAAM,gBAAgB,mvBAkB5B,CAAC;AAoQF,QAAA,MAAM,aAAa,EAAE,YAkCpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -36,20 +36,26 @@ function normalizePBXValue(value) {
36
36
  function findPBXGroupKeyByName(xcodeProject, name) {
37
37
  var _a;
38
38
  const pbxGroups = (_a = xcodeProject.hash.project.objects['PBXGroup']) !== null && _a !== void 0 ? _a : {};
39
+ // Prefer a group whose `path` equals the name — that group IS the filesystem
40
+ // directory. A group matched only by `name` may be a virtual (path-less)
41
+ // container whose children resolve relative to its parent, not itself.
42
+ let nameMatch = null;
39
43
  for (const key of Object.keys(pbxGroups)) {
40
44
  if (key.endsWith('_comment'))
41
45
  continue;
42
46
  const group = pbxGroups[key];
43
- if (normalizePBXValue(group.name) === name ||
44
- normalizePBXValue(group.path) === name) {
47
+ if (normalizePBXValue(group.path) === name)
45
48
  return key;
49
+ if (normalizePBXValue(group.name) === name && nameMatch === null) {
50
+ nameMatch = key;
46
51
  }
47
52
  }
48
- return null;
53
+ return nameMatch;
49
54
  }
50
- function findPBXFileReference(xcodeProject, filename) {
55
+ function findPBXFileReferences(xcodeProject, filename) {
51
56
  var _a;
52
57
  const refs = (_a = xcodeProject.hash.project.objects['PBXFileReference']) !== null && _a !== void 0 ? _a : {};
58
+ const matches = [];
53
59
  for (const key of Object.keys(refs)) {
54
60
  if (key.endsWith('_comment'))
55
61
  continue;
@@ -60,13 +66,16 @@ function findPBXFileReference(xcodeProject, filename) {
60
66
  const refName = normalizePBXValue(ref.name);
61
67
  if (path.posix.basename(refPath) === filename ||
62
68
  path.posix.basename(refName) === filename) {
63
- return { key, ref };
69
+ matches.push({ key, ref });
64
70
  }
65
71
  }
66
- return null;
72
+ return matches;
67
73
  }
68
- function removeFileReferenceFromAllGroups(xcodeProject, fileRefKey) {
74
+ function removeFileReferenceFromAllGroups(xcodeProject, fileRefKeys) {
69
75
  var _a;
76
+ if (fileRefKeys.length === 0)
77
+ return;
78
+ const fileRefKeySet = new Set(fileRefKeys);
70
79
  const pbxGroups = (_a = xcodeProject.hash.project.objects['PBXGroup']) !== null && _a !== void 0 ? _a : {};
71
80
  for (const key of Object.keys(pbxGroups)) {
72
81
  if (key.endsWith('_comment'))
@@ -74,32 +83,110 @@ function removeFileReferenceFromAllGroups(xcodeProject, fileRefKey) {
74
83
  const group = pbxGroups[key];
75
84
  if (!Array.isArray(group.children))
76
85
  continue;
77
- group.children = group.children.filter((child) => child.value !== fileRefKey);
86
+ group.children = group.children.filter((child) => { var _a; return !fileRefKeySet.has((_a = child.value) !== null && _a !== void 0 ? _a : ''); });
78
87
  }
79
88
  }
80
- function ensureIOSPluginSourceFile(xcodeProject, projectName) {
89
+ function findPBXBuildFileKeys(xcodeProject, fileRefKeys, filename) {
90
+ var _a;
91
+ const fileRefKeySet = new Set(fileRefKeys);
92
+ const buildFiles = (_a = xcodeProject.hash.project.objects['PBXBuildFile']) !== null && _a !== void 0 ? _a : {};
93
+ return Object.keys(buildFiles).filter((key) => {
94
+ if (key.endsWith('_comment'))
95
+ return false;
96
+ const buildFile = buildFiles[key];
97
+ if (typeof buildFile !== 'object' || buildFile === null)
98
+ return false;
99
+ return (fileRefKeySet.has(buildFile.fileRef) ||
100
+ normalizePBXValue(buildFile.fileRef_comment) === filename);
101
+ });
102
+ }
103
+ function removePBXBuildFiles(xcodeProject, buildFileKeys) {
81
104
  var _a;
105
+ if (buildFileKeys.length === 0)
106
+ return;
107
+ const buildFiles = (_a = xcodeProject.hash.project.objects['PBXBuildFile']) !== null && _a !== void 0 ? _a : {};
108
+ for (const key of buildFileKeys) {
109
+ delete buildFiles[key];
110
+ delete buildFiles[`${key}_comment`];
111
+ }
112
+ }
113
+ function removePBXSourcesBuildPhaseEntries(xcodeProject, buildFileKeys, filename) {
114
+ var _a;
115
+ if (buildFileKeys.length === 0)
116
+ return;
117
+ const buildFileKeySet = new Set(buildFileKeys);
118
+ const sourcePhases = (_a = xcodeProject.hash.project.objects['PBXSourcesBuildPhase']) !== null && _a !== void 0 ? _a : {};
119
+ for (const key of Object.keys(sourcePhases)) {
120
+ if (key.endsWith('_comment'))
121
+ continue;
122
+ const phase = sourcePhases[key];
123
+ if (!Array.isArray(phase.files))
124
+ continue;
125
+ phase.files = phase.files.filter((file) => {
126
+ var _a;
127
+ return !buildFileKeySet.has((_a = file.value) !== null && _a !== void 0 ? _a : '') &&
128
+ normalizePBXValue(file.comment) !== `${filename} in Sources`;
129
+ });
130
+ }
131
+ }
132
+ function removePBXFileReferences(xcodeProject, fileReferences) {
133
+ var _a;
134
+ if (fileReferences.length === 0)
135
+ return;
136
+ const refs = (_a = xcodeProject.hash.project.objects['PBXFileReference']) !== null && _a !== void 0 ? _a : {};
137
+ for (const { key } of fileReferences) {
138
+ delete refs[key];
139
+ delete refs[`${key}_comment`];
140
+ }
141
+ }
142
+ function ensureIOSPluginSourceFile(xcodeProject, projectName) {
143
+ var _a, _b, _c;
82
144
  const groupKey = (_a = findPBXGroupKeyByName(xcodeProject, projectName)) !== null && _a !== void 0 ? _a : undefined;
83
- const existingFileReference = findPBXFileReference(xcodeProject, IOS_PLUGIN_FILENAME);
84
- if (existingFileReference) {
145
+ // A virtual (name-only) PBXGroup has no `path`. Files added to it with
146
+ // `<group>` sourceTree resolve relative to the group's *parent*, not the
147
+ // group itself, so `BeaconGeoPlugin.swift` would land at `ios/` not
148
+ // `ios/<AppName>/`. Only treat the group as a physical directory group when
149
+ // its `path` actually equals the project name.
150
+ const pbxGroups = (_b = xcodeProject.hash.project.objects['PBXGroup']) !== null && _b !== void 0 ? _b : {};
151
+ const groupHasPhysicalPath = groupKey != null &&
152
+ normalizePBXValue((_c = pbxGroups[groupKey]) === null || _c === void 0 ? void 0 : _c.path) === projectName;
153
+ const existingFileReferences = findPBXFileReferences(xcodeProject, IOS_PLUGIN_FILENAME);
154
+ const existingBuildFileKeys = findPBXBuildFileKeys(xcodeProject, existingFileReferences.map(({ key }) => key), IOS_PLUGIN_FILENAME);
155
+ if (existingFileReferences.length === 1 &&
156
+ existingBuildFileKeys.length === 1) {
157
+ const [existingFileReference] = existingFileReferences;
85
158
  existingFileReference.ref.name = `"${IOS_PLUGIN_FILENAME}"`;
86
159
  existingFileReference.ref.sourceTree = '"<group>"';
87
160
  if (groupKey) {
88
- removeFileReferenceFromAllGroups(xcodeProject, existingFileReference.key);
161
+ removeFileReferenceFromAllGroups(xcodeProject, [existingFileReference.key]);
89
162
  xcodeProject.addToPbxGroup({
90
163
  fileRef: existingFileReference.key,
91
164
  basename: IOS_PLUGIN_FILENAME,
92
165
  }, groupKey);
93
- existingFileReference.ref.path = `"${IOS_PLUGIN_FILENAME}"`;
166
+ // If the group is virtual (no path), the file reference path must include
167
+ // the project name so Xcode resolves it to ios/<AppName>/BeaconGeoPlugin.swift.
168
+ existingFileReference.ref.path = groupHasPhysicalPath
169
+ ? `"${IOS_PLUGIN_FILENAME}"`
170
+ : `"${projectName}/${IOS_PLUGIN_FILENAME}"`;
94
171
  return;
95
172
  }
96
173
  existingFileReference.ref.path = `"${projectName}/${IOS_PLUGIN_FILENAME}"`;
97
174
  return;
98
175
  }
99
- if (groupKey) {
176
+ if (existingFileReferences.length > 0 || existingBuildFileKeys.length > 0) {
177
+ removePBXSourcesBuildPhaseEntries(xcodeProject, existingBuildFileKeys, IOS_PLUGIN_FILENAME);
178
+ removePBXBuildFiles(xcodeProject, existingBuildFileKeys);
179
+ removeFileReferenceFromAllGroups(xcodeProject, existingFileReferences.map(({ key }) => key));
180
+ removePBXFileReferences(xcodeProject, existingFileReferences);
181
+ }
182
+ if (groupKey && groupHasPhysicalPath) {
183
+ // Physical group (path = projectName): add with just the filename; Xcode
184
+ // resolves it through the group's directory chain to ios/<AppName>/file.
100
185
  xcodeProject.addSourceFile(IOS_PLUGIN_FILENAME, null, groupKey);
101
186
  return;
102
187
  }
188
+ // Virtual group (name-only, no path) or no group found: use an explicit
189
+ // SOURCE_ROOT-relative path so Xcode always finds ios/<AppName>/file.
103
190
  xcodeProject.addSourceFile(`${projectName}/${IOS_PLUGIN_FILENAME}`);
104
191
  }
105
192
  function modifySwiftAppDelegate(contents) {