react-native-pointr 10.7.0 → 10.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [10.7.1] - 2026-08-05
8
+
9
+ ### Changed
10
+ - Mobile SDK 10.7.0 integration.
11
+
12
+
7
13
  ## [10.7.0] - 2026-07-31
8
14
 
9
15
  ### Changed
@@ -32,6 +38,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
38
 
33
39
  ### Added
34
40
  - **`PointrSdk` methods** exposing previously native-only capabilities: `shouldRequestPermissionsAtStartup`, `requestPermissions`, `getPois`, `searchPois`, `getSites`, `getBuildings`, `getSiteByExternalId`, `getClientName`, `isWayfindingReady`, `calculateDistance`, `isMyCarMarked`.
41
+ - **Shared bridge key constants** — every string crossing the JS ↔ native boundary for a map widget action (action types, `action` and `sdkConfig` payload keys, view manager command names) is now declared in one constants file per platform: `src/constants/bridgeKeys.ts`, `android/src/main/java/com/pointr/PTRBridgeKeys.kt` and `ios/PTRBridgeKeys.swift`. No literals remain in the action serializer or in either native reader, so a key can no longer be renamed on one side only. `PTRActionParamKeys`, `PTRSdkConfigKeys` and `PTRCommandNames` are exported from the package.
42
+
43
+ ### Changed
44
+ - **Actions serialize through `toPayload()`** — `PTRAction` subclasses now build their JSON payload from `PTRActionParamKeys` instead of the component spreading their class fields, so a field rename can no longer silently change the wire format.
45
+
46
+ ### Fixed
47
+ - **Imperative `focusCoordinate` and `showMyCar` on Android** — the native side matched `focusCoordinate` / `showMyCarForSite` while JS dispatches `coordinate` / `myCarForSite`, so both commands were silently ignored. All three platforms now use the shared command names.
48
+ - **`markMyCar` / `showMyCar` via the declarative `action` prop on Android** — the completion event read an argument slot the declarative path never pushed, throwing inside the callback. Argument reads are now type-checked with defaults.
49
+ - **`personaId` in the `sdkConfig` prop on iOS** — the key was written by JS but never read, so the persona was ignored when the widget initialised the SDK.
35
50
 
36
51
 
37
52
  ## [10.3.0] - 2026-06-30
package/EXTENDING.md CHANGED
@@ -358,6 +358,62 @@ Ensure both iOS and Android implementations return the same data structure:
358
358
  }
359
359
  ```
360
360
 
361
+ ### 7. Never Hard-code Bridge Strings
362
+
363
+ Map widget actions cross the bridge as JSON, so a key that JS writes and native
364
+ reads is matched by string only — rename it on one side and the other side
365
+ silently reads an empty value, with no compile error anywhere. Every such string
366
+ lives in a constants file, one per language, and the three are kept identical:
367
+
368
+ | Side | File |
369
+ | ---------- | --------------------------------------------------- |
370
+ | TypeScript | `src/constants/bridgeKeys.ts` |
371
+ | Android | `android/src/main/java/com/pointr/PTRBridgeKeys.kt` |
372
+ | iOS | `ios/PTRBridgeKeys.swift` |
373
+
374
+ They cover four groups:
375
+
376
+ - **Action types** — the `type` discriminator of the `action` prop
377
+ - **Action param keys** — every other field of the `action` prop payload
378
+ - **SDK config keys** — the fields of the `sdkConfig` prop
379
+ - **Command names** — the view manager commands `executeMapAction` dispatches
380
+
381
+ When adding an action or a parameter:
382
+
383
+ 1. Add the constant to all three files (same name, same value).
384
+ 2. In TypeScript, build the payload from `PTRActionParamKeys` by overriding
385
+ `toPayload()` on the action class — never rely on the class field names.
386
+ 3. Read it back through `PTRBridgeKeys.ActionParamKeys` in both Kotlin and Swift.
387
+
388
+ ```typescript
389
+ // src/actions/index.ts
390
+ export class PTRMyNewAction extends PTRAction {
391
+ constructor(readonly site: string, readonly poi: string) {
392
+ super(PTRActionType.MY_NEW_ACTION);
393
+ }
394
+
395
+ override toPayload(): Record<string, unknown> {
396
+ return {
397
+ ...super.toPayload(),
398
+ [Keys.SITE]: this.site,
399
+ [Keys.POI]: this.poi,
400
+ };
401
+ }
402
+ }
403
+ ```
404
+
405
+ ```kotlin
406
+ // PTRMapWidgetManager.kt
407
+ val site = obj.optString(ActionParamKeys.SITE, "")
408
+ val poi = obj.optString(ActionParamKeys.POI, "")
409
+ ```
410
+
411
+ ```swift
412
+ // PTRMapWidgetContainerView.swift
413
+ let siteId = json[Keys.site] as? String ?? ""
414
+ let poiId = json[Keys.poi] as? String ?? ""
415
+ ```
416
+
361
417
  ## Adding Event Listeners
362
418
 
363
419
  For continuous data streams or callbacks, use event emitters:
@@ -0,0 +1,153 @@
1
+ package com.pointr
2
+
3
+ import androidx.annotation.StringDef
4
+
5
+ /**
6
+ * Every string that crosses the JS <-> native boundary when an action is sent
7
+ * to the map widget: action type discriminators, `action` / `sdkConfig` payload
8
+ * keys and view manager command names.
9
+ *
10
+ * Nothing here may be hard-coded anywhere else — a key renamed on one side only
11
+ * fails silently (`optString` returns an empty string), so all three sides read
12
+ * them from a constants file:
13
+ *
14
+ * TypeScript src/constants/bridgeKeys.ts
15
+ * Android android/src/main/java/com/pointr/PTRBridgeKeys.kt (this file)
16
+ * iOS ios/PTRBridgeKeys.swift
17
+ *
18
+ * Changing a value here means changing it in the other two files as well.
19
+ */
20
+ object PTRBridgeKeys {
21
+
22
+ /** Discriminator written to the `type` field of the serialized `action` prop. */
23
+ object ActionTypes {
24
+ /** Focus the map on a site, building or level */
25
+ const val FOCUS_MAP = "focusMap"
26
+ /** Highlight a single POI */
27
+ const val HIGHLIGHT_POI = "highlightPoi"
28
+ /** Draw a static route between two POIs */
29
+ const val DISPLAY_ROUTE = "displayRoute"
30
+ /** Start live wayfinding to a destination POI */
31
+ const val START_WAYFINDING = "startWayfinding"
32
+ /** Mark the parked car location */
33
+ const val MARK_MY_CAR = "markMyCar"
34
+ /** Show the previously marked car location */
35
+ const val SHOW_MY_CAR = "showMyCar"
36
+ /** Focus the map on a geographic coordinate */
37
+ const val FOCUS_COORDINATE = "focusCoordinate"
38
+ /** Highlight POIs of one or more categories */
39
+ const val HIGHLIGHT_CATEGORY = "highlightCategory"
40
+ }
41
+
42
+ /**
43
+ * Field names of the JSON payload carried by the `action` view prop.
44
+ * JS writes them, Android and iOS read them back out.
45
+ */
46
+ object ActionParamKeys {
47
+ /** Action discriminator — one of [ActionTypes] */
48
+ const val TYPE = "type"
49
+ /** Site external identifier */
50
+ const val SITE = "site"
51
+ /** Building external identifier */
52
+ const val BUILDING = "building"
53
+ /** Level index used by focusMap / markMyCar */
54
+ const val LEVEL = "level"
55
+ /** POI external identifier — destination for highlightPoi and startWayfinding */
56
+ const val POI = "poi"
57
+ /** Source POI external identifier for displayRoute */
58
+ const val FROM_POI = "fromPoi"
59
+ /** Destination POI external identifier for displayRoute */
60
+ const val TO_POI = "toPoi"
61
+ /** Whether the car popup is shown */
62
+ const val SHOULD_SHOW_POPUP = "shouldShowPopup"
63
+ /** Animation applied to the car marker */
64
+ const val ANIMATION_TYPE = "animationType"
65
+ /** Latitude for focusCoordinate */
66
+ const val LATITUDE = "latitude"
67
+ /** Longitude for focusCoordinate */
68
+ const val LONGITUDE = "longitude"
69
+ /** Level index for focusCoordinate */
70
+ const val LEVEL_INDEX = "levelIndex"
71
+ /** Category identifiers for highlightCategory */
72
+ const val CATEGORY_IDS = "categoryIds"
73
+ }
74
+
75
+ /** Field names of the JSON payload carried by the `sdkConfig` view prop. */
76
+ object SdkConfigKeys {
77
+ /** Client identifier issued by Pointr */
78
+ const val CLIENT_ID = "clientId"
79
+ /** License key issued by Pointr */
80
+ const val LICENSE_KEY = "licenseKey"
81
+ /** Base URL of the Pointr environment */
82
+ const val BASE_URL = "baseUrl"
83
+ /** Persona identifier applied to the SDK params */
84
+ const val PERSONA_ID = "personaId"
85
+ /** Log level ordinal */
86
+ const val LOG_LEVEL = "logLevel"
87
+ }
88
+
89
+ /**
90
+ * View manager command names used by the imperative `executeMapAction` path.
91
+ * JS dispatches them, Android matches them in `receiveCommand` and iOS
92
+ * exports them as `@objc` methods — the names must match on all three sides.
93
+ */
94
+ object CommandNames {
95
+ /** Focus the map on a site */
96
+ const val FOCUS_SITE = "site"
97
+ /** Focus the map on a building */
98
+ const val FOCUS_BUILDING = "building"
99
+ /** Focus the map on a level */
100
+ const val FOCUS_LEVEL = "level"
101
+ /** Highlight a POI */
102
+ const val HIGHLIGHT_POI = "poi"
103
+ /** Draw a static route between two POIs */
104
+ const val DISPLAY_ROUTE = "displayRoute"
105
+ /** Start live wayfinding */
106
+ const val START_WAYFINDING = "startWayfinding"
107
+ /** Mark the car at site level */
108
+ const val MARK_MY_CAR_FOR_SITE = "markMyCarForSite"
109
+ /** Mark the car at a specific level */
110
+ const val MARK_MY_CAR_FOR_LEVEL = "markMyCarForLevel"
111
+ /** Show the marked car at site level */
112
+ const val SHOW_MY_CAR_FOR_SITE = "myCarForSite"
113
+ /** Focus the map on a coordinate */
114
+ const val FOCUS_COORDINATE = "coordinate"
115
+ /** Highlight a category */
116
+ const val HIGHLIGHT_CATEGORY = "highlightCategory"
117
+ }
118
+ }
119
+
120
+ /** Restricts a String parameter to the [PTRBridgeKeys.ActionTypes] values. */
121
+ @Retention(AnnotationRetention.SOURCE)
122
+ @StringDef(
123
+ value = [
124
+ PTRBridgeKeys.ActionTypes.FOCUS_MAP,
125
+ PTRBridgeKeys.ActionTypes.HIGHLIGHT_POI,
126
+ PTRBridgeKeys.ActionTypes.DISPLAY_ROUTE,
127
+ PTRBridgeKeys.ActionTypes.START_WAYFINDING,
128
+ PTRBridgeKeys.ActionTypes.MARK_MY_CAR,
129
+ PTRBridgeKeys.ActionTypes.SHOW_MY_CAR,
130
+ PTRBridgeKeys.ActionTypes.FOCUS_COORDINATE,
131
+ PTRBridgeKeys.ActionTypes.HIGHLIGHT_CATEGORY
132
+ ]
133
+ )
134
+ annotation class PTRActionTypeDef
135
+
136
+ /** Restricts a String parameter to the [PTRBridgeKeys.CommandNames] values. */
137
+ @Retention(AnnotationRetention.SOURCE)
138
+ @StringDef(
139
+ value = [
140
+ PTRBridgeKeys.CommandNames.FOCUS_SITE,
141
+ PTRBridgeKeys.CommandNames.FOCUS_BUILDING,
142
+ PTRBridgeKeys.CommandNames.FOCUS_LEVEL,
143
+ PTRBridgeKeys.CommandNames.HIGHLIGHT_POI,
144
+ PTRBridgeKeys.CommandNames.DISPLAY_ROUTE,
145
+ PTRBridgeKeys.CommandNames.START_WAYFINDING,
146
+ PTRBridgeKeys.CommandNames.MARK_MY_CAR_FOR_SITE,
147
+ PTRBridgeKeys.CommandNames.MARK_MY_CAR_FOR_LEVEL,
148
+ PTRBridgeKeys.CommandNames.SHOW_MY_CAR_FOR_SITE,
149
+ PTRBridgeKeys.CommandNames.FOCUS_COORDINATE,
150
+ PTRBridgeKeys.CommandNames.HIGHLIGHT_CATEGORY
151
+ ]
152
+ )
153
+ annotation class PTRCommandDef
@@ -8,12 +8,17 @@ import android.widget.FrameLayout
8
8
  import androidx.fragment.app.FragmentActivity
9
9
  import com.facebook.react.bridge.ReactApplicationContext
10
10
  import com.facebook.react.bridge.ReadableArray
11
+ import com.facebook.react.bridge.ReadableType
11
12
  import com.facebook.react.bridge.WritableMap
12
13
  import com.facebook.react.bridge.WritableNativeMap
13
14
  import com.facebook.react.uimanager.ThemedReactContext
14
15
  import com.facebook.react.uimanager.ViewGroupManager
15
16
  import com.facebook.react.uimanager.annotations.ReactProp
16
17
  import com.facebook.react.uimanager.events.RCTEventEmitter
18
+ import com.pointr.PTRBridgeKeys.ActionParamKeys
19
+ import com.pointr.PTRBridgeKeys.ActionTypes
20
+ import com.pointr.PTRBridgeKeys.CommandNames
21
+ import com.pointr.PTRBridgeKeys.SdkConfigKeys
17
22
  import com.pointrlabs.core.management.Pointr
18
23
  import com.pointrlabs.core.management.interfaces.PointrListener
19
24
  import com.pointrlabs.core.management.models.PTRParams
@@ -57,9 +62,10 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
57
62
  // auto-executes it once the view is attached rather than requiring a JS
58
63
  // imperative action dispatch.
59
64
  //
60
- // JSON shape: { "type": "focusMap"|"highlightPoi"|"displayRoute"|"startWayfinding"|"markMyCar"|"showMyCar"|"focusCoordinate"|"highlightCategory",
61
- // "site": String, "building"?: String, "level"?: Int,
62
- // "poi"?: String, "fromPoi"?: String, "toPoi"?: String }
65
+ // The JSON shape is described by PTRBridgeKeys.ActionTypes (the `type`
66
+ // discriminator) and PTRBridgeKeys.ActionParamKeys (every other field).
67
+ // Read the payload only through those constants — the JS side writes it
68
+ // from the matching src/constants/bridgeKeys.ts.
63
69
  private var pendingActionJson: String? = null
64
70
  private var pendingSdkConfigJson: String? = null
65
71
  private var actionDidExecute = false
@@ -102,16 +108,16 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
102
108
  val json = pendingSdkConfigJson ?: return
103
109
  try {
104
110
  val obj = org.json.JSONObject(json)
105
- val clientId = obj.optString("clientId")
106
- val licenseKey = obj.optString("licenseKey")
107
- val baseUrl = obj.optString("baseUrl")
108
- val personaId = obj.optString("personaId", "")
111
+ val clientId = obj.optString(SdkConfigKeys.CLIENT_ID)
112
+ val licenseKey = obj.optString(SdkConfigKeys.LICENSE_KEY)
113
+ val baseUrl = obj.optString(SdkConfigKeys.BASE_URL)
114
+ val personaId = obj.optString(SdkConfigKeys.PERSONA_ID, "")
109
115
  if (clientId.isNotEmpty() && licenseKey.isNotEmpty() && baseUrl.isNotEmpty()) {
110
116
  val params = PTRParams(clientId, licenseKey, baseUrl)
111
117
  if (personaId.isNotEmpty()) {
112
118
  params.personaId = personaId
113
119
  }
114
- val logLevel = obj.optInt("logLevel", Plog.LogLevel.ERROR.ordinal)
120
+ val logLevel = obj.optInt(SdkConfigKeys.LOG_LEVEL, Plog.LogLevel.ERROR.ordinal)
115
121
  params.logLevel = try {
116
122
  Plog.LogLevel.entries[logLevel]
117
123
  } catch (_: Exception) {
@@ -150,69 +156,74 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
150
156
  }
151
157
  try {
152
158
  val obj = org.json.JSONObject(json)
153
- val type = obj.optString("type")
154
- val site = obj.optString("site", "")
155
- val building = obj.optString("building", "")
156
- val level = obj.optInt("level", 0)
157
- val poi = obj.optString("poi", "")
158
- val fromPoi = obj.optString("fromPoi", "")
159
- val toPoi = obj.optString("toPoi", "")
159
+ val type = obj.optString(ActionParamKeys.TYPE)
160
+ val site = obj.optString(ActionParamKeys.SITE, "")
161
+ val building = obj.optString(ActionParamKeys.BUILDING, "")
162
+ val level = obj.optInt(ActionParamKeys.LEVEL, 0)
163
+ val poi = obj.optString(ActionParamKeys.POI, "")
164
+ val fromPoi = obj.optString(ActionParamKeys.FROM_POI, "")
165
+ val toPoi = obj.optString(ActionParamKeys.TO_POI, "")
160
166
 
161
167
  val args = com.facebook.react.bridge.Arguments.createArray()
162
- val shouldShowPopup = obj.optBoolean("shouldShowPopup", true)
168
+ val shouldShowPopup = obj.optBoolean(ActionParamKeys.SHOULD_SHOW_POPUP, true)
169
+ val animationType = obj.optInt(ActionParamKeys.ANIMATION_TYPE, 1)
163
170
  when (type) {
164
- PTRMapWidgetActionType.FOCUS_MAP -> {
165
- if (building.isNotEmpty() && obj.has("level")) {
171
+ ActionTypes.FOCUS_MAP -> {
172
+ if (building.isNotEmpty() && obj.has(ActionParamKeys.LEVEL)) {
166
173
  args.pushString(site); args.pushString(building); args.pushInt(level)
167
- executeCommand("level", args)
174
+ executeCommand(CommandNames.FOCUS_LEVEL, args)
168
175
  } else if (building.isNotEmpty()) {
169
176
  args.pushString(site); args.pushString(building)
170
- executeCommand("building", args)
177
+ executeCommand(CommandNames.FOCUS_BUILDING, args)
171
178
  } else {
172
179
  args.pushString(site)
173
- executeCommand("site", args)
180
+ executeCommand(CommandNames.FOCUS_SITE, args)
174
181
  }
175
182
  }
176
- PTRMapWidgetActionType.HIGHLIGHT_POI -> {
183
+ ActionTypes.HIGHLIGHT_POI -> {
177
184
  args.pushString(site); args.pushString(poi)
178
- executeCommand("poi", args)
185
+ executeCommand(CommandNames.HIGHLIGHT_POI, args)
179
186
  }
180
- PTRMapWidgetActionType.DISPLAY_ROUTE -> {
187
+ ActionTypes.DISPLAY_ROUTE -> {
181
188
  args.pushString(site); args.pushString(fromPoi); args.pushString(toPoi)
182
- executeCommand(PTRMapWidgetActionType.DISPLAY_ROUTE, args)
189
+ executeCommand(CommandNames.DISPLAY_ROUTE, args)
183
190
  }
184
- PTRMapWidgetActionType.START_WAYFINDING -> {
185
- args.pushString(site); args.pushString(toPoi)
186
- executeCommand(PTRMapWidgetActionType.START_WAYFINDING, args)
191
+ ActionTypes.START_WAYFINDING -> {
192
+ // PTRStartWayfindingAction serialises its destination under
193
+ // ActionParamKeys.POI; TO_POI stays accepted as a fallback for
194
+ // hand-written, deeplink-style payloads.
195
+ args.pushString(site); args.pushString(poi.ifEmpty { toPoi })
196
+ executeCommand(CommandNames.START_WAYFINDING, args)
187
197
  }
188
- PTRMapWidgetActionType.MARK_MY_CAR -> {
189
- if (building.isNotEmpty() && obj.has("level")) {
190
- args.pushString(site); args.pushString(building); args.pushInt(level); args.pushBoolean(shouldShowPopup)
191
- executeCommand("markMyCarForLevel", args)
198
+ ActionTypes.MARK_MY_CAR -> {
199
+ if (building.isNotEmpty() && obj.has(ActionParamKeys.LEVEL)) {
200
+ args.pushString(site); args.pushString(building); args.pushInt(level)
201
+ args.pushBoolean(shouldShowPopup); args.pushInt(animationType)
202
+ executeCommand(CommandNames.MARK_MY_CAR_FOR_LEVEL, args)
192
203
  } else {
193
- args.pushString(site); args.pushBoolean(shouldShowPopup)
194
- executeCommand("markMyCarForSite", args)
204
+ args.pushString(site); args.pushBoolean(shouldShowPopup); args.pushInt(animationType)
205
+ executeCommand(CommandNames.MARK_MY_CAR_FOR_SITE, args)
195
206
  }
196
207
  }
197
- PTRMapWidgetActionType.SHOW_MY_CAR -> {
198
- args.pushString(site); args.pushBoolean(shouldShowPopup)
199
- executeCommand("showMyCarForSite", args)
208
+ ActionTypes.SHOW_MY_CAR -> {
209
+ args.pushString(site); args.pushBoolean(shouldShowPopup); args.pushInt(animationType)
210
+ executeCommand(CommandNames.SHOW_MY_CAR_FOR_SITE, args)
200
211
  }
201
- PTRMapWidgetActionType.FOCUS_COORDINATE -> {
202
- val lat = obj.optDouble("latitude", 0.0)
203
- val lon = obj.optDouble("longitude", 0.0)
204
- val lvl = obj.optInt("levelIndex", -1)
212
+ ActionTypes.FOCUS_COORDINATE -> {
213
+ val lat = obj.optDouble(ActionParamKeys.LATITUDE, 0.0)
214
+ val lon = obj.optDouble(ActionParamKeys.LONGITUDE, 0.0)
215
+ val lvl = obj.optInt(ActionParamKeys.LEVEL_INDEX, -1)
205
216
  args.pushString(site); args.pushDouble(lat); args.pushDouble(lon); args.pushInt(lvl)
206
- executeCommand(PTRMapWidgetActionType.FOCUS_COORDINATE, args)
217
+ executeCommand(CommandNames.FOCUS_COORDINATE, args)
207
218
  }
208
- PTRMapWidgetActionType.HIGHLIGHT_CATEGORY -> {
219
+ ActionTypes.HIGHLIGHT_CATEGORY -> {
209
220
  val catArr = com.facebook.react.bridge.Arguments.createArray()
210
- val jsonArr = obj.optJSONArray("categoryIds")
221
+ val jsonArr = obj.optJSONArray(ActionParamKeys.CATEGORY_IDS)
211
222
  if (jsonArr != null) {
212
223
  for (i in 0 until jsonArr.length()) catArr.pushString(jsonArr.optString(i))
213
224
  }
214
225
  args.pushString(site); args.pushArray(catArr)
215
- executeCommand(PTRMapWidgetActionType.HIGHLIGHT_CATEGORY, args)
226
+ executeCommand(CommandNames.HIGHLIGHT_CATEGORY, args)
216
227
  }
217
228
  else -> Log.w(name, "Unknown action type in action prop: $type")
218
229
  }
@@ -272,17 +283,17 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
272
283
  }
273
284
 
274
285
  private fun executeCommand(
275
- command: String,
286
+ @PTRCommandDef command: String,
276
287
  args: ReadableArray
277
288
  ) {
278
289
  val action: PTRMapWidgetAction = when (command) {
279
- "site" -> {
290
+ CommandNames.FOCUS_SITE -> {
280
291
  val siteId = args.getString(0).orEmpty()
281
292
  val location = PTRMapWidgetSiteLocation(PTRIdentifier(siteId, isExternal = true))
282
293
  PTRMapWidgetAction.focusMap(location)
283
294
  }
284
295
 
285
- "building" -> {
296
+ CommandNames.FOCUS_BUILDING -> {
286
297
  val siteId = args.getString(0).orEmpty()
287
298
  val buildingId = args.getString(1).orEmpty()
288
299
  val location = PTRMapWidgetBuildingLocation(
@@ -292,7 +303,7 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
292
303
  PTRMapWidgetAction.focusMap(location)
293
304
  }
294
305
 
295
- "level" -> {
306
+ CommandNames.FOCUS_LEVEL -> {
296
307
  val siteId = args.getString(0).orEmpty()
297
308
  val buildingId = args.getString(1).orEmpty()
298
309
  val location = PTRMapWidgetLevelLocation(
@@ -303,7 +314,7 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
303
314
  PTRMapWidgetAction.focusMap(location)
304
315
  }
305
316
 
306
- "poi" -> {
317
+ CommandNames.HIGHLIGHT_POI -> {
307
318
  val siteId = args.getString(0).orEmpty()
308
319
  val poiId = args.getString(1).orEmpty()
309
320
  val location = PTRMapWidgetPoiLocation(
@@ -313,7 +324,7 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
313
324
  PTRMapWidgetAction.highlightPoi(location)
314
325
  }
315
326
 
316
- PTRMapWidgetActionType.START_WAYFINDING -> {
327
+ CommandNames.START_WAYFINDING -> {
317
328
  val siteId = args.getString(0).orEmpty()
318
329
  val poiId = args.getString(1).orEmpty()
319
330
  val location = PTRMapWidgetPoiLocation(
@@ -323,7 +334,7 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
323
334
  PTRMapWidgetAction.startWayfinding(location)
324
335
  }
325
336
 
326
- PTRMapWidgetActionType.DISPLAY_ROUTE -> {
337
+ CommandNames.DISPLAY_ROUTE -> {
327
338
  val siteId = args.getString(0).orEmpty()
328
339
  val sourcePoiId = args.getString(1).orEmpty()
329
340
  val destinationPoiId = args.getString(2).orEmpty()
@@ -338,13 +349,13 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
338
349
  PTRMapWidgetAction.displayRoute(sourceLocation, destinationLocation)
339
350
  }
340
351
 
341
- "markMyCarForSite" -> {
352
+ CommandNames.MARK_MY_CAR_FOR_SITE -> {
342
353
  val siteId = args.getString(0).orEmpty()
343
354
  val location = PTRMapWidgetSiteLocation(PTRIdentifier(siteId, isExternal = true))
344
355
  PTRMapWidgetAction.markMyCar(location, args.getBoolean(1))
345
356
  }
346
357
 
347
- "markMyCarForLevel" -> {
358
+ CommandNames.MARK_MY_CAR_FOR_LEVEL -> {
348
359
  val siteId = args.getString(0).orEmpty()
349
360
  val buildingId = args.getString(1).orEmpty()
350
361
  val location = PTRMapWidgetLevelLocation(
@@ -355,13 +366,18 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
355
366
  PTRMapWidgetMarkMyCarAction(location, shouldShowPopup = args.getBoolean(3))
356
367
  }
357
368
 
358
- "showMyCarForSite" -> {
369
+ CommandNames.SHOW_MY_CAR_FOR_SITE -> {
359
370
  val siteId = args.getString(0).orEmpty()
360
371
  val location = PTRMapWidgetSiteLocation(PTRIdentifier(siteId, isExternal = true))
361
- PTRMapWidgetAction.showMyCar(location, shouldShowPopup = args.getBoolean(1))
372
+ // The declarative path sends (site, shouldShowPopup, animationType) while the
373
+ // imperative JS path sends (site, animationType) — read index 1 by type so
374
+ // neither shape throws.
375
+ val shouldShowPopup =
376
+ if (args.size() > 1 && args.getType(1) == ReadableType.Boolean) args.getBoolean(1) else true
377
+ PTRMapWidgetAction.showMyCar(location, shouldShowPopup = shouldShowPopup)
362
378
  }
363
379
 
364
- PTRMapWidgetActionType.FOCUS_COORDINATE -> {
380
+ CommandNames.FOCUS_COORDINATE -> {
365
381
  val siteId = args.getString(0).orEmpty()
366
382
  val latitude = args.getDouble(1)
367
383
  val longitude = args.getDouble(2)
@@ -376,7 +392,7 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
376
392
  PTRMapWidgetAction.focusMap(location)
377
393
  }
378
394
 
379
- PTRMapWidgetActionType.HIGHLIGHT_CATEGORY -> {
395
+ CommandNames.HIGHLIGHT_CATEGORY -> {
380
396
  val siteId = args.getString(0).orEmpty()
381
397
  val categoryIds = mutableListOf<String>()
382
398
  val arr = args.getArray(1)
@@ -401,77 +417,80 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
401
417
  getMapWidgetFragment(action)
402
418
  }
403
419
 
420
+ // The command arguments are positional, and the two dispatch paths (declarative
421
+ // `action` prop vs imperative executeMapAction) do not always push the same
422
+ // number of them. Read them by type with a fallback so a shorter payload
423
+ // degrades to a default instead of throwing inside the completion callback.
424
+ private fun argString(args: Array<out Any>, index: Int) = args.getOrNull(index) as? String ?: ""
425
+ private fun argInt(args: Array<out Any>, index: Int) = (args.getOrNull(index) as? Number)?.toInt() ?: 0
426
+ private fun argBoolean(args: Array<out Any>, index: Int) = args.getOrNull(index) as? Boolean ?: true
427
+
428
+ /** The error is always appended as the last argument by [executeCommand]. */
429
+ private fun argError(args: Array<out Any>) = args.lastOrNull() as? String ?: ""
430
+
404
431
  private fun mapWidgetDidEndLoading(
405
- @PTRMapWidgetActionType command: String,
432
+ @PTRCommandDef command: String,
406
433
  vararg args: Any
407
434
  ) {
408
435
  val event: WritableMap = WritableNativeMap()
409
436
  event.putString("command", command)
410
437
  when (command) {
411
- "site" -> {
412
- event.putString("siteExternalIdentifier", args[0] as String)
413
- event.putString("error", args[1] as String)
438
+ CommandNames.FOCUS_SITE -> {
439
+ event.putString("siteExternalIdentifier", argString(args, 0))
414
440
  }
415
441
 
416
- "building" -> {
417
- event.putString("siteExternalIdentifier", args[0] as String)
418
- event.putString("buildingExternalIdentifier", args[1] as String)
419
- event.putString("error", args[2] as String)
442
+ CommandNames.FOCUS_BUILDING -> {
443
+ event.putString("siteExternalIdentifier", argString(args, 0))
444
+ event.putString("buildingExternalIdentifier", argString(args, 1))
420
445
  }
421
446
 
422
- "level" -> {
423
- event.putString("siteExternalIdentifier", args[0] as String)
424
- event.putString("buildingExternalIdentifier", args[1] as String)
425
- event.putInt("levelIndex", (args[2] as Number).toInt())
426
- event.putString("error", args[3] as String)
447
+ CommandNames.FOCUS_LEVEL -> {
448
+ event.putString("siteExternalIdentifier", argString(args, 0))
449
+ event.putString("buildingExternalIdentifier", argString(args, 1))
450
+ event.putInt("levelIndex", argInt(args, 2))
427
451
  }
428
452
 
429
- "poi" -> {
430
- event.putString("siteExternalIdentifier", args[0] as String)
431
- event.putString("poiExternalIdentifier", args[1] as String)
432
- event.putString("error", args[2] as String)
453
+ CommandNames.HIGHLIGHT_POI -> {
454
+ event.putString("siteExternalIdentifier", argString(args, 0))
455
+ event.putString("poiExternalIdentifier", argString(args, 1))
433
456
  }
434
457
 
435
- PTRMapWidgetActionType.START_WAYFINDING -> {
436
- event.putString("siteExternalIdentifier", args[0] as String)
437
- event.putString("poiExternalIdentifier", args[1] as String)
438
- event.putString("error", args[2] as String)
458
+ CommandNames.START_WAYFINDING -> {
459
+ event.putString("siteExternalIdentifier", argString(args, 0))
460
+ event.putString("poiExternalIdentifier", argString(args, 1))
439
461
  }
440
462
 
441
- PTRMapWidgetActionType.DISPLAY_ROUTE -> {
442
- event.putString("siteExternalIdentifier", args[0] as String)
443
- event.putString("fromPoiExternalIdentifier", args[1] as String)
444
- event.putString("toPoiExternalIdentifier", args[2] as String)
445
- event.putString("error", args[3] as String)
463
+ CommandNames.DISPLAY_ROUTE -> {
464
+ event.putString("siteExternalIdentifier", argString(args, 0))
465
+ event.putString("fromPoiExternalIdentifier", argString(args, 1))
466
+ event.putString("toPoiExternalIdentifier", argString(args, 2))
446
467
  }
447
468
 
448
- "markMyCarForSite" -> {
449
- event.putString("siteExternalIdentifier", args[0] as String)
450
- event.putBoolean("shouldShowPopup", args[1] as Boolean)
451
- event.putInt("animationType", (args[2] as Number).toInt())
452
- event.putString("error", args[3] as String)
469
+ CommandNames.MARK_MY_CAR_FOR_SITE -> {
470
+ event.putString("siteExternalIdentifier", argString(args, 0))
471
+ event.putBoolean("shouldShowPopup", argBoolean(args, 1))
472
+ event.putInt("animationType", argInt(args, 2))
453
473
  }
454
474
 
455
- "markMyCarForLevel" -> {
456
- event.putString("siteExternalIdentifier", args[0] as String)
457
- event.putString("buildingExternalIdentifier", args[1] as String)
458
- event.putInt("levelIndex", (args[2] as Number).toInt())
459
- event.putBoolean("shouldShowPopup", args[3] as Boolean)
460
- event.putInt("animationType", (args[4] as Number).toInt())
461
- event.putString("error", args[5] as String)
475
+ CommandNames.MARK_MY_CAR_FOR_LEVEL -> {
476
+ event.putString("siteExternalIdentifier", argString(args, 0))
477
+ event.putString("buildingExternalIdentifier", argString(args, 1))
478
+ event.putInt("levelIndex", argInt(args, 2))
479
+ event.putBoolean("shouldShowPopup", argBoolean(args, 3))
480
+ event.putInt("animationType", argInt(args, 4))
462
481
  }
463
482
 
464
- "showMyCarForSite" -> {
465
- event.putString("siteExternalIdentifier", args[0] as String)
466
- event.putBoolean("shouldShowPopup", args[1] as Boolean)
467
- event.putInt("animationType", (args[2] as Number).toInt())
468
- event.putString("error", args[3] as String)
483
+ CommandNames.SHOW_MY_CAR_FOR_SITE -> {
484
+ event.putString("siteExternalIdentifier", argString(args, 0))
485
+ event.putBoolean("shouldShowPopup", argBoolean(args, 1))
486
+ event.putInt("animationType", argInt(args, 2))
469
487
  }
470
488
 
471
489
  else -> {
472
490
  return
473
491
  }
474
492
  }
493
+ event.putString("error", argError(args))
475
494
  Log.v(name, "sending onMapWidgetDidEndLoading with event: $event")
476
495
  reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(
477
496
  frameLayout.id,