expo-accessory-setup 0.0.1 → 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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +267 -6
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/accessorysetup/AccessorySetupException.kt +20 -0
- package/android/src/main/java/expo/modules/accessorysetup/CompanionAssociations.kt +152 -0
- package/android/src/main/java/expo/modules/accessorysetup/ExpoAccessorySetupModule.kt +214 -0
- package/android/src/main/java/expo/modules/accessorysetup/Records.kt +31 -0
- package/app.plugin.js +1 -0
- package/build/ExpoAccessorySetup.types.d.ts +63 -0
- package/build/ExpoAccessorySetup.types.d.ts.map +1 -0
- package/build/ExpoAccessorySetup.types.js +2 -0
- package/build/ExpoAccessorySetup.types.js.map +1 -0
- package/build/ExpoAccessorySetupModule.d.ts +24 -0
- package/build/ExpoAccessorySetupModule.d.ts.map +1 -0
- package/build/ExpoAccessorySetupModule.js +4 -0
- package/build/ExpoAccessorySetupModule.js.map +1 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +45 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +20 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +61 -0
- package/build/index.js.map +1 -0
- package/build/pickerOptions.d.ts +20 -0
- package/build/pickerOptions.d.ts.map +1 -0
- package/build/pickerOptions.js +64 -0
- package/build/pickerOptions.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AccessorySessionController.swift +290 -0
- package/ios/AccessorySetupException.swift +50 -0
- package/ios/AccessorySetupRecords.swift +32 -0
- package/ios/ExpoAccessorySetup.podspec +31 -0
- package/ios/ExpoAccessorySetupModule.swift +67 -0
- package/mocks/ExpoAccessorySetup.ts +13 -0
- package/package.json +63 -4
- package/plugin/build/index.d.ts +8 -0
- package/plugin/build/index.js +85 -0
- package/plugin/build/options.d.ts +29 -0
- package/plugin/build/options.js +88 -0
- package/plugin/src/index.ts +122 -0
- package/plugin/src/options.ts +142 -0
- package/plugin/tsconfig.json +18 -0
- package/src/ExpoAccessorySetup.types.ts +69 -0
- package/src/ExpoAccessorySetupModule.ts +20 -0
- package/src/errors.ts +59 -0
- package/src/index.ts +87 -0
- package/src/pickerOptions.ts +100 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
package expo.modules.accessorysetup
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.companion.AssociationInfo
|
|
5
|
+
import android.companion.CompanionDeviceManager
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.content.IntentSender
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.activity.result.IntentSenderRequest
|
|
11
|
+
import androidx.annotation.RequiresApi
|
|
12
|
+
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
|
13
|
+
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
|
|
14
|
+
import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher
|
|
15
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
16
|
+
import expo.modules.kotlin.functions.Coroutine
|
|
17
|
+
import expo.modules.kotlin.modules.Module
|
|
18
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
19
|
+
import kotlinx.coroutines.CompletableDeferred
|
|
20
|
+
import kotlinx.coroutines.Dispatchers
|
|
21
|
+
import kotlinx.coroutines.withContext
|
|
22
|
+
import java.io.Serializable
|
|
23
|
+
|
|
24
|
+
private const val ACCESSORY_ADDED = "onAccessoryAdded"
|
|
25
|
+
private const val ACCESSORY_REMOVED = "onAccessoryRemoved"
|
|
26
|
+
private const val ACCESSORY_CHANGED = "onAccessoryChanged"
|
|
27
|
+
|
|
28
|
+
class ExpoAccessorySetupModule : Module() {
|
|
29
|
+
private lateinit var associationLauncher: AppContextActivityResultLauncher<AssociationInput, AssociationOutput>
|
|
30
|
+
|
|
31
|
+
// Main-thread confined: set by showPicker, completed by CompanionDeviceManager callbacks,
|
|
32
|
+
// the association activity result, or the activity being destroyed.
|
|
33
|
+
private var pendingAssociation: PendingAssociation? = null
|
|
34
|
+
|
|
35
|
+
private val context: Context
|
|
36
|
+
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
37
|
+
|
|
38
|
+
// Cheap to create: the system service and shared preferences are cached by the platform.
|
|
39
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
40
|
+
private fun associations() = CompanionAssociations(context)
|
|
41
|
+
|
|
42
|
+
override fun definition() = ModuleDefinition {
|
|
43
|
+
Name("ExpoAccessorySetup")
|
|
44
|
+
|
|
45
|
+
Events(ACCESSORY_ADDED, ACCESSORY_REMOVED, ACCESSORY_CHANGED)
|
|
46
|
+
|
|
47
|
+
RegisterActivityContracts {
|
|
48
|
+
associationLauncher = registerForActivityResult(AssociationContract)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Function("isSupported") {
|
|
52
|
+
isCompanionDeviceSetupSupported(context)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
AsyncFunction("showPicker") Coroutine { options: PickerOptions ->
|
|
56
|
+
if (!isCompanionDeviceSetupSupported(context)) {
|
|
57
|
+
throw unsupported()
|
|
58
|
+
}
|
|
59
|
+
withContext(Dispatchers.Main) { showPicker(options).toMap() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
AsyncFunction("getPairedAccessories") Coroutine { ->
|
|
63
|
+
if (!isCompanionDeviceSetupSupported(context)) {
|
|
64
|
+
throw unsupported()
|
|
65
|
+
}
|
|
66
|
+
associations().list().map(PairedAccessory::toMap)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
AsyncFunction("removeAccessory") Coroutine { id: String ->
|
|
70
|
+
if (!isCompanionDeviceSetupSupported(context)) {
|
|
71
|
+
throw unsupported()
|
|
72
|
+
}
|
|
73
|
+
val accessory = associations().remove(id)
|
|
74
|
+
sendEvent(ACCESSORY_REMOVED, mapOf("accessory" to accessory.toMap()))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
OnActivityDestroys {
|
|
78
|
+
pendingAssociation?.fail(
|
|
79
|
+
AccessorySetupException(
|
|
80
|
+
AccessorySetupErrorCode.UNKNOWN,
|
|
81
|
+
"The activity was destroyed while the companion device picker was showing."
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun unsupported() = AccessorySetupException(
|
|
88
|
+
AccessorySetupErrorCode.UNSUPPORTED,
|
|
89
|
+
"This device doesn't support companion device setup."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
93
|
+
private suspend fun showPicker(options: PickerOptions): PairedAccessory {
|
|
94
|
+
if (pendingAssociation != null) {
|
|
95
|
+
throw AccessorySetupException(
|
|
96
|
+
AccessorySetupErrorCode.ALREADY_IN_PROGRESS,
|
|
97
|
+
"Another companion device picker is already showing."
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
val association = PendingAssociation()
|
|
101
|
+
pendingAssociation = association
|
|
102
|
+
try {
|
|
103
|
+
associations().associate(options, AssociationCallback(association))
|
|
104
|
+
val accessory = association.result.await()
|
|
105
|
+
associations().remember(accessory)
|
|
106
|
+
sendEvent(ACCESSORY_ADDED, mapOf("accessory" to accessory.toMap()))
|
|
107
|
+
return accessory
|
|
108
|
+
} finally {
|
|
109
|
+
pendingAssociation = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
114
|
+
private fun launchAssociationActivity(association: PendingAssociation, intentSender: IntentSender) {
|
|
115
|
+
if (association.activityLaunched || association.result.isCompleted) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
association.activityLaunched = true
|
|
119
|
+
try {
|
|
120
|
+
associationLauncher.launch(AssociationInput(intentSender)) { output ->
|
|
121
|
+
onAssociationActivityResult(association, output)
|
|
122
|
+
}
|
|
123
|
+
} catch (e: Exception) {
|
|
124
|
+
association.fail(AccessorySetupException(AccessorySetupErrorCode.UNKNOWN, "Couldn't show the companion device picker.", e))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
129
|
+
private fun onAssociationActivityResult(association: PendingAssociation, output: AssociationOutput) {
|
|
130
|
+
if (output.resultCode != Activity.RESULT_OK) {
|
|
131
|
+
association.fail(exceptionForResultCode(output.resultCode, null))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
val accessory = output.data?.let(associations()::fromActivityResult)
|
|
135
|
+
if (accessory == null) {
|
|
136
|
+
association.fail(AccessorySetupException(AccessorySetupErrorCode.UNKNOWN, "The picker returned no Bluetooth device."))
|
|
137
|
+
} else {
|
|
138
|
+
association.result.complete(accessory)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
143
|
+
private inner class AssociationCallback(private val association: PendingAssociation) : CompanionDeviceManager.Callback() {
|
|
144
|
+
override fun onAssociationPending(intentSender: IntentSender) {
|
|
145
|
+
launchAssociationActivity(association, intentSender)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Deprecated("Replaced by onAssociationPending on API 33")
|
|
149
|
+
override fun onDeviceFound(intentSender: IntentSender) {
|
|
150
|
+
launchAssociationActivity(association, intentSender)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override fun onAssociationCreated(associationInfo: AssociationInfo) {
|
|
154
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
155
|
+
associations().toAccessory(associationInfo)?.let(association.result::complete)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun onFailure(errorCode: Int, error: CharSequence?) {
|
|
160
|
+
association.fail(exceptionForResultCode(errorCode, error))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun onFailure(error: CharSequence?) {
|
|
164
|
+
// From API 36 the coded overload above fires as well and carries the reason. Once the
|
|
165
|
+
// association activity is showing, its result reports the outcome instead.
|
|
166
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA || association.activityLaunched) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
association.fail(
|
|
170
|
+
AccessorySetupException(AccessorySetupErrorCode.UNKNOWN, error?.toString() ?: "Companion device association failed.")
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private class PendingAssociation {
|
|
177
|
+
val result = CompletableDeferred<PairedAccessory>()
|
|
178
|
+
var activityLaunched = false
|
|
179
|
+
|
|
180
|
+
fun fail(exception: AccessorySetupException) {
|
|
181
|
+
result.completeExceptionally(exception)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun exceptionForResultCode(resultCode: Int, error: CharSequence?): AccessorySetupException {
|
|
186
|
+
val code = when (resultCode) {
|
|
187
|
+
CompanionDeviceManager.RESULT_CANCELED, CompanionDeviceManager.RESULT_USER_REJECTED ->
|
|
188
|
+
AccessorySetupErrorCode.USER_CANCELLED
|
|
189
|
+
CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT -> AccessorySetupErrorCode.DISCOVERY_TIMEOUT
|
|
190
|
+
else -> AccessorySetupErrorCode.UNKNOWN
|
|
191
|
+
}
|
|
192
|
+
val message = error?.toString() ?: when (code) {
|
|
193
|
+
AccessorySetupErrorCode.USER_CANCELLED -> "The user dismissed the companion device picker."
|
|
194
|
+
AccessorySetupErrorCode.DISCOVERY_TIMEOUT -> "No matching device was found."
|
|
195
|
+
else -> "Companion device association failed with result code $resultCode."
|
|
196
|
+
}
|
|
197
|
+
return AccessorySetupException(code, message)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The intent sender can't be persisted, so the activity-recreation fallback receives `null`. */
|
|
201
|
+
internal class AssociationInput(@Transient val intentSender: IntentSender? = null) : Serializable
|
|
202
|
+
|
|
203
|
+
internal class AssociationOutput(val resultCode: Int, val data: Intent?)
|
|
204
|
+
|
|
205
|
+
internal object AssociationContract : AppContextActivityResultContract<AssociationInput, AssociationOutput> {
|
|
206
|
+
override fun createIntent(context: Context, input: AssociationInput): Intent {
|
|
207
|
+
val request = IntentSenderRequest.Builder(requireNotNull(input.intentSender)).build()
|
|
208
|
+
return Intent(StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST)
|
|
209
|
+
.putExtra(StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST, request)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
override fun parseResult(input: AssociationInput, resultCode: Int, intent: Intent?) =
|
|
213
|
+
AssociationOutput(resultCode, intent)
|
|
214
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package expo.modules.accessorysetup
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
|
|
6
|
+
internal class PickerOptions : Record {
|
|
7
|
+
/** Always the 128-bit form; the JS layer expands 16- and 32-bit UUIDs. */
|
|
8
|
+
@Field
|
|
9
|
+
val serviceUUID: String? = null
|
|
10
|
+
|
|
11
|
+
@Field
|
|
12
|
+
val companyIdentifier: Int? = null
|
|
13
|
+
|
|
14
|
+
@Field
|
|
15
|
+
val nameSubstring: String? = null
|
|
16
|
+
|
|
17
|
+
@Field
|
|
18
|
+
val singleDevice: Boolean = true
|
|
19
|
+
|
|
20
|
+
@Field
|
|
21
|
+
val namePattern: String? = null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal data class PairedAccessory(val macAddress: String, val displayName: String) {
|
|
25
|
+
fun toMap() = mapOf(
|
|
26
|
+
"id" to macAddress,
|
|
27
|
+
"bleIdentifier" to macAddress,
|
|
28
|
+
"displayName" to displayName,
|
|
29
|
+
"platform" to "android"
|
|
30
|
+
)
|
|
31
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build');
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type AccessoryPlatform = 'ios' | 'android';
|
|
2
|
+
export type PairedAccessory = {
|
|
3
|
+
/**
|
|
4
|
+
* Stable identifier for `removeAccessory()` and for matching events.
|
|
5
|
+
* Equal to `bleIdentifier` for Bluetooth accessories.
|
|
6
|
+
*/
|
|
7
|
+
id: string;
|
|
8
|
+
/**
|
|
9
|
+
* Pass this to react-native-ble-plx (`connectToDevice`, `devices`, ...).
|
|
10
|
+
* iOS: the CoreBluetooth peripheral UUID. Android: the MAC address, upper case.
|
|
11
|
+
*/
|
|
12
|
+
bleIdentifier: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
platform: AccessoryPlatform;
|
|
15
|
+
};
|
|
16
|
+
export type AccessoryFilter = {
|
|
17
|
+
/**
|
|
18
|
+
* 16-bit (`"180D"`), 32-bit, or 128-bit Bluetooth service UUID.
|
|
19
|
+
* Must also be listed in the config plugin's `ios.bluetoothServices`.
|
|
20
|
+
*/
|
|
21
|
+
serviceUUID?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Bluetooth SIG company identifier, 0 to 0xFFFF.
|
|
24
|
+
* Must also be listed in the config plugin's `ios.bluetoothCompanyIdentifiers`.
|
|
25
|
+
*/
|
|
26
|
+
companyIdentifier?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Matches accessories whose advertised name contains this string.
|
|
29
|
+
* Must also be listed in the config plugin's `ios.bluetoothNames`.
|
|
30
|
+
*/
|
|
31
|
+
nameSubstring?: string;
|
|
32
|
+
};
|
|
33
|
+
export type ImageSource = number | {
|
|
34
|
+
uri: string;
|
|
35
|
+
};
|
|
36
|
+
export type IosPickerOptions = {
|
|
37
|
+
/** Product name shown in the system picker. */
|
|
38
|
+
displayName: string;
|
|
39
|
+
/** Product image shown in the system picker: `require('./accessory.png')` or `{ uri }`. */
|
|
40
|
+
productImage: ImageSource;
|
|
41
|
+
/** `immediate` only discovers accessories held close to the device. Defaults to `default`. */
|
|
42
|
+
range?: 'default' | 'immediate';
|
|
43
|
+
};
|
|
44
|
+
export type AndroidPickerOptions = {
|
|
45
|
+
/** Stop scanning at the first match and show a single-device confirmation. Defaults to `true`. */
|
|
46
|
+
singleDevice?: boolean;
|
|
47
|
+
/** Java regex matched against the advertised name. Replaces `filter.nameSubstring` on Android. */
|
|
48
|
+
namePattern?: string;
|
|
49
|
+
};
|
|
50
|
+
export type ShowPickerOptions = {
|
|
51
|
+
/** Requires `serviceUUID` or `companyIdentifier`. */
|
|
52
|
+
filter: AccessoryFilter;
|
|
53
|
+
ios: IosPickerOptions;
|
|
54
|
+
android?: AndroidPickerOptions;
|
|
55
|
+
};
|
|
56
|
+
export type AccessoryEventType = 'accessoryAdded' | 'accessoryRemoved' | 'accessoryChanged';
|
|
57
|
+
export type AccessoryEvent = {
|
|
58
|
+
accessory: PairedAccessory;
|
|
59
|
+
};
|
|
60
|
+
export type AccessorySubscription = {
|
|
61
|
+
remove(): void;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=ExpoAccessorySetup.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAccessorySetup.types.d.ts","sourceRoot":"","sources":["../src/ExpoAccessorySetup.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,SAAS,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,YAAY,EAAE,WAAW,CAAC;IAC1B,8FAA8F;IAC9F,KAAK,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,kGAAkG;IAClG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qDAAqD;IACrD,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,gBAAgB,CAAC;IACtB,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAE5F,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,eAAe,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAccessorySetup.types.js","sourceRoot":"","sources":["../src/ExpoAccessorySetup.types.ts"],"names":[],"mappings":"","sourcesContent":["export type AccessoryPlatform = 'ios' | 'android';\n\nexport type PairedAccessory = {\n /**\n * Stable identifier for `removeAccessory()` and for matching events.\n * Equal to `bleIdentifier` for Bluetooth accessories.\n */\n id: string;\n /**\n * Pass this to react-native-ble-plx (`connectToDevice`, `devices`, ...).\n * iOS: the CoreBluetooth peripheral UUID. Android: the MAC address, upper case.\n */\n bleIdentifier: string;\n displayName: string;\n platform: AccessoryPlatform;\n};\n\nexport type AccessoryFilter = {\n /**\n * 16-bit (`\"180D\"`), 32-bit, or 128-bit Bluetooth service UUID.\n * Must also be listed in the config plugin's `ios.bluetoothServices`.\n */\n serviceUUID?: string;\n /**\n * Bluetooth SIG company identifier, 0 to 0xFFFF.\n * Must also be listed in the config plugin's `ios.bluetoothCompanyIdentifiers`.\n */\n companyIdentifier?: number;\n /**\n * Matches accessories whose advertised name contains this string.\n * Must also be listed in the config plugin's `ios.bluetoothNames`.\n */\n nameSubstring?: string;\n};\n\nexport type ImageSource = number | { uri: string };\n\nexport type IosPickerOptions = {\n /** Product name shown in the system picker. */\n displayName: string;\n /** Product image shown in the system picker: `require('./accessory.png')` or `{ uri }`. */\n productImage: ImageSource;\n /** `immediate` only discovers accessories held close to the device. Defaults to `default`. */\n range?: 'default' | 'immediate';\n};\n\nexport type AndroidPickerOptions = {\n /** Stop scanning at the first match and show a single-device confirmation. Defaults to `true`. */\n singleDevice?: boolean;\n /** Java regex matched against the advertised name. Replaces `filter.nameSubstring` on Android. */\n namePattern?: string;\n};\n\nexport type ShowPickerOptions = {\n /** Requires `serviceUUID` or `companyIdentifier`. */\n filter: AccessoryFilter;\n ios: IosPickerOptions;\n android?: AndroidPickerOptions;\n};\n\nexport type AccessoryEventType = 'accessoryAdded' | 'accessoryRemoved' | 'accessoryChanged';\n\nexport type AccessoryEvent = {\n accessory: PairedAccessory;\n};\n\nexport type AccessorySubscription = {\n remove(): void;\n};\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import type { PairedAccessory } from './ExpoAccessorySetup.types';
|
|
3
|
+
import type { NativePickerOptions } from './pickerOptions';
|
|
4
|
+
export type ExpoAccessorySetupModuleEvents = {
|
|
5
|
+
onAccessoryAdded: (event: {
|
|
6
|
+
accessory: PairedAccessory;
|
|
7
|
+
}) => void;
|
|
8
|
+
onAccessoryRemoved: (event: {
|
|
9
|
+
accessory: PairedAccessory;
|
|
10
|
+
}) => void;
|
|
11
|
+
onAccessoryChanged: (event: {
|
|
12
|
+
accessory: PairedAccessory;
|
|
13
|
+
}) => void;
|
|
14
|
+
};
|
|
15
|
+
declare class ExpoAccessorySetupModule extends NativeModule<ExpoAccessorySetupModuleEvents> {
|
|
16
|
+
isSupported(): boolean;
|
|
17
|
+
showPicker(options: NativePickerOptions): Promise<PairedAccessory>;
|
|
18
|
+
getPairedAccessories(): Promise<PairedAccessory[]>;
|
|
19
|
+
removeAccessory(id: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** `null` in Expo Go, on web, and in builds that don't include the module. */
|
|
22
|
+
declare const _default: ExpoAccessorySetupModule | null;
|
|
23
|
+
export default _default;
|
|
24
|
+
//# sourceMappingURL=ExpoAccessorySetupModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAccessorySetupModule.d.ts","sourceRoot":"","sources":["../src/ExpoAccessorySetupModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA+B,MAAM,MAAM,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,MAAM,8BAA8B,GAAG;IAC3C,gBAAgB,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,eAAe,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,kBAAkB,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,eAAe,CAAA;KAAE,KAAK,IAAI,CAAC;IACpE,kBAAkB,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,eAAe,CAAA;KAAE,KAAK,IAAI,CAAC;CACrE,CAAC;AAEF,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,8BAA8B,CAAC;IACzF,WAAW,IAAI,OAAO;IACtB,UAAU,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAClE,oBAAoB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAClD,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAC3C;AAED,8EAA8E;;AAC9E,wBAA2F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAccessorySetupModule.js","sourceRoot":"","sources":["../src/ExpoAccessorySetupModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,2BAA2B,EAAE,MAAM,MAAM,CAAC;AAkBjE,8EAA8E;AAC9E,eAAe,2BAA2B,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireOptionalNativeModule } from 'expo';\n\nimport type { PairedAccessory } from './ExpoAccessorySetup.types';\nimport type { NativePickerOptions } from './pickerOptions';\n\nexport type ExpoAccessorySetupModuleEvents = {\n onAccessoryAdded: (event: { accessory: PairedAccessory }) => void;\n onAccessoryRemoved: (event: { accessory: PairedAccessory }) => void;\n onAccessoryChanged: (event: { accessory: PairedAccessory }) => void;\n};\n\ndeclare class ExpoAccessorySetupModule extends NativeModule<ExpoAccessorySetupModuleEvents> {\n isSupported(): boolean;\n showPicker(options: NativePickerOptions): Promise<PairedAccessory>;\n getPairedAccessories(): Promise<PairedAccessory[]>;\n removeAccessory(id: string): Promise<void>;\n}\n\n/** `null` in Expo Go, on web, and in builds that don't include the module. */\nexport default requireOptionalNativeModule<ExpoAccessorySetupModule>('ExpoAccessorySetup');\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const AccessorySetupErrorCodes: readonly ["UNSUPPORTED", "USER_CANCELLED", "PERMISSION_DENIED", "ALREADY_IN_PROGRESS", "NOT_FOUND", "DISCOVERY_TIMEOUT", "INVALID_OPTIONS", "UNKNOWN"];
|
|
2
|
+
export type AccessorySetupErrorCode = (typeof AccessorySetupErrorCodes)[number];
|
|
3
|
+
export declare class AccessorySetupError extends Error {
|
|
4
|
+
readonly code: AccessorySetupErrorCode;
|
|
5
|
+
constructor(code: AccessorySetupErrorCode, message: string, options?: {
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export declare function isAccessorySetupError(error: unknown, code?: AccessorySetupErrorCode): error is AccessorySetupError;
|
|
10
|
+
/** Native modules reject with `{ code, message }`. Anything unrecognized becomes `UNKNOWN`. */
|
|
11
|
+
export declare function toAccessorySetupError(error: unknown): AccessorySetupError;
|
|
12
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,wJAS3B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;gBAE3B,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK1F;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,uBAAuB,GAC7B,KAAK,IAAI,mBAAmB,CAE9B;AAmBD,+FAA+F;AAC/F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAUzE"}
|
package/build/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const AccessorySetupErrorCodes = [
|
|
2
|
+
'UNSUPPORTED',
|
|
3
|
+
'USER_CANCELLED',
|
|
4
|
+
'PERMISSION_DENIED',
|
|
5
|
+
'ALREADY_IN_PROGRESS',
|
|
6
|
+
'NOT_FOUND',
|
|
7
|
+
'DISCOVERY_TIMEOUT',
|
|
8
|
+
'INVALID_OPTIONS',
|
|
9
|
+
'UNKNOWN',
|
|
10
|
+
];
|
|
11
|
+
export class AccessorySetupError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = 'AccessorySetupError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isAccessorySetupError(error, code) {
|
|
20
|
+
return error instanceof AccessorySetupError && (code === undefined || error.code === code);
|
|
21
|
+
}
|
|
22
|
+
function isErrorCode(value) {
|
|
23
|
+
return AccessorySetupErrorCodes.includes(value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Expo wraps the module's exception in a function call exception, so the module's reason is the
|
|
27
|
+
* first cause: `Calling 'x' failed (at …)\n→ Caused by: AccessorySetupException: <reason> (at File.swift:22)`.
|
|
28
|
+
* Debug iOS builds add the exception name and Swift source location, which are stripped here.
|
|
29
|
+
*/
|
|
30
|
+
function nativeReason(message) {
|
|
31
|
+
const [, reason] = message.split('\n→ Caused by: ');
|
|
32
|
+
if (reason === undefined) {
|
|
33
|
+
return message;
|
|
34
|
+
}
|
|
35
|
+
return reason.replace(/^\w+Exception: /, '').replace(/ \(at [^()]*\)$/, '');
|
|
36
|
+
}
|
|
37
|
+
/** Native modules reject with `{ code, message }`. Anything unrecognized becomes `UNKNOWN`. */
|
|
38
|
+
export function toAccessorySetupError(error) {
|
|
39
|
+
if (error instanceof AccessorySetupError) {
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
const { code, message } = (error ?? {});
|
|
43
|
+
return new AccessorySetupError(isErrorCode(code) ? code : 'UNKNOWN', typeof message === 'string' ? nativeReason(message) : String(error), { cause: error });
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,aAAa;IACb,gBAAgB;IAChB,mBAAmB;IACnB,qBAAqB;IACrB,WAAW;IACX,mBAAmB;IACnB,iBAAiB;IACjB,SAAS;CACD,CAAC;AAIX,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,IAAI,CAA0B;IAEvC,YAAY,IAA6B,EAAE,OAAe,EAAE,OAA6B;QACvF,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,UAAU,qBAAqB,CACnC,KAAc,EACd,IAA8B;IAE9B,OAAO,KAAK,YAAY,mBAAmB,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC7F,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAQ,wBAA+C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACpD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,KAAK,YAAY,mBAAmB,EAAE,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,CAA0C,CAAC;IACjF,OAAO,IAAI,mBAAmB,CAC5B,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EACpC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACnE,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;AACJ,CAAC","sourcesContent":["export const AccessorySetupErrorCodes = [\n 'UNSUPPORTED',\n 'USER_CANCELLED',\n 'PERMISSION_DENIED',\n 'ALREADY_IN_PROGRESS',\n 'NOT_FOUND',\n 'DISCOVERY_TIMEOUT',\n 'INVALID_OPTIONS',\n 'UNKNOWN',\n] as const;\n\nexport type AccessorySetupErrorCode = (typeof AccessorySetupErrorCodes)[number];\n\nexport class AccessorySetupError extends Error {\n readonly code: AccessorySetupErrorCode;\n\n constructor(code: AccessorySetupErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'AccessorySetupError';\n this.code = code;\n }\n}\n\nexport function isAccessorySetupError(\n error: unknown,\n code?: AccessorySetupErrorCode\n): error is AccessorySetupError {\n return error instanceof AccessorySetupError && (code === undefined || error.code === code);\n}\n\nfunction isErrorCode(value: unknown): value is AccessorySetupErrorCode {\n return (AccessorySetupErrorCodes as readonly unknown[]).includes(value);\n}\n\n/**\n * Expo wraps the module's exception in a function call exception, so the module's reason is the\n * first cause: `Calling 'x' failed (at …)\\n→ Caused by: AccessorySetupException: <reason> (at File.swift:22)`.\n * Debug iOS builds add the exception name and Swift source location, which are stripped here.\n */\nfunction nativeReason(message: string): string {\n const [, reason] = message.split('\\n→ Caused by: ');\n if (reason === undefined) {\n return message;\n }\n return reason.replace(/^\\w+Exception: /, '').replace(/ \\(at [^()]*\\)$/, '');\n}\n\n/** Native modules reject with `{ code, message }`. Anything unrecognized becomes `UNKNOWN`. */\nexport function toAccessorySetupError(error: unknown): AccessorySetupError {\n if (error instanceof AccessorySetupError) {\n return error;\n }\n const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown };\n return new AccessorySetupError(\n isErrorCode(code) ? code : 'UNKNOWN',\n typeof message === 'string' ? nativeReason(message) : String(error),\n { cause: error }\n );\n}\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AccessoryEvent, AccessoryEventType, AccessorySubscription, PairedAccessory, ShowPickerOptions } from './ExpoAccessorySetup.types';
|
|
2
|
+
export * from './ExpoAccessorySetup.types';
|
|
3
|
+
export { AccessorySetupError, AccessorySetupErrorCodes, isAccessorySetupError, type AccessorySetupErrorCode, } from './errors';
|
|
4
|
+
/** `true` on iOS 18+ and on Android devices with the companion device setup feature. */
|
|
5
|
+
export declare function isSupported(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Shows the system accessory picker and resolves with the accessory the user selected.
|
|
8
|
+
* Rejects with `USER_CANCELLED` when the user dismisses the picker and `ALREADY_IN_PROGRESS`
|
|
9
|
+
* when another picker is showing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function showPicker(options: ShowPickerOptions): Promise<PairedAccessory>;
|
|
12
|
+
export declare function getPairedAccessories(): Promise<PairedAccessory[]>;
|
|
13
|
+
/** Rejects with `NOT_FOUND` when no paired accessory has this `id`. */
|
|
14
|
+
export declare function removeAccessory(id: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* On Android, events only fire for changes made through this module. Accessories removed from
|
|
17
|
+
* system settings disappear from the next `getPairedAccessories()` result without an event.
|
|
18
|
+
*/
|
|
19
|
+
export declare function addAccessoryListener(type: AccessoryEventType, listener: (event: AccessoryEvent) => void): AccessorySubscription;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAKpC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAC;AAsBlB,wFAAwF;AACxF,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAM/E;AAED,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAEjE;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzD;AAQD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,kBAAkB,EACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GACxC,qBAAqB,CAKvB"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import NativeModule from './ExpoAccessorySetupModule';
|
|
3
|
+
import { AccessorySetupError, toAccessorySetupError } from './errors';
|
|
4
|
+
import { toNativePickerOptions } from './pickerOptions';
|
|
5
|
+
export * from './ExpoAccessorySetup.types';
|
|
6
|
+
export { AccessorySetupError, AccessorySetupErrorCodes, isAccessorySetupError, } from './errors';
|
|
7
|
+
function requireModule() {
|
|
8
|
+
if (!NativeModule || !NativeModule.isSupported()) {
|
|
9
|
+
throw new AccessorySetupError('UNSUPPORTED', NativeModule
|
|
10
|
+
? 'Accessory setup is not supported on this device (iOS 18+ or Android with companion device setup is required).'
|
|
11
|
+
: 'The ExpoAccessorySetup native module is not available. Use a development build, not Expo Go.');
|
|
12
|
+
}
|
|
13
|
+
return NativeModule;
|
|
14
|
+
}
|
|
15
|
+
async function call(body) {
|
|
16
|
+
try {
|
|
17
|
+
return await body();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw toAccessorySetupError(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** `true` on iOS 18+ and on Android devices with the companion device setup feature. */
|
|
24
|
+
export function isSupported() {
|
|
25
|
+
return NativeModule?.isSupported() ?? false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shows the system accessory picker and resolves with the accessory the user selected.
|
|
29
|
+
* Rejects with `USER_CANCELLED` when the user dismisses the picker and `ALREADY_IN_PROGRESS`
|
|
30
|
+
* when another picker is showing.
|
|
31
|
+
*/
|
|
32
|
+
export function showPicker(options) {
|
|
33
|
+
return call(() => {
|
|
34
|
+
const module = requireModule();
|
|
35
|
+
const platform = Platform.OS === 'ios' ? 'ios' : 'android';
|
|
36
|
+
return module.showPicker(toNativePickerOptions(options, platform));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function getPairedAccessories() {
|
|
40
|
+
return call(() => requireModule().getPairedAccessories());
|
|
41
|
+
}
|
|
42
|
+
/** Rejects with `NOT_FOUND` when no paired accessory has this `id`. */
|
|
43
|
+
export function removeAccessory(id) {
|
|
44
|
+
return call(() => requireModule().removeAccessory(id));
|
|
45
|
+
}
|
|
46
|
+
const nativeEventNames = {
|
|
47
|
+
accessoryAdded: 'onAccessoryAdded',
|
|
48
|
+
accessoryRemoved: 'onAccessoryRemoved',
|
|
49
|
+
accessoryChanged: 'onAccessoryChanged',
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* On Android, events only fire for changes made through this module. Accessories removed from
|
|
53
|
+
* system settings disappear from the next `getPairedAccessories()` result without an event.
|
|
54
|
+
*/
|
|
55
|
+
export function addAccessoryListener(type, listener) {
|
|
56
|
+
if (!NativeModule) {
|
|
57
|
+
return { remove() { } };
|
|
58
|
+
}
|
|
59
|
+
return NativeModule.addListener(nativeEventNames[type], listener);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AASxC,OAAO,YAAY,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAExD,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,GAEtB,MAAM,UAAU,CAAC;AAElB,SAAS,aAAa;IACpB,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,mBAAmB,CAC3B,aAAa,EACb,YAAY;YACV,CAAC,CAAC,+GAA+G;YACjH,CAAC,CAAC,8FAA8F,CACnG,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,KAAK,UAAU,IAAI,CAAI,IAAsB;IAC3C,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW;IACzB,OAAO,YAAY,EAAE,WAAW,EAAE,IAAI,KAAK,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,OAAO,IAAI,CAAC,GAAG,EAAE;QACf,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3D,OAAO,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,oBAAoB,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,gBAAgB,GAAG;IACvB,cAAc,EAAE,kBAAkB;IAClC,gBAAgB,EAAE,oBAAoB;IACtC,gBAAgB,EAAE,oBAAoB;CACe,CAAC;AAExD;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAwB,EACxB,QAAyC;IAEzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,KAAI,CAAC,EAAE,CAAC;IACzB,CAAC;IACD,OAAO,YAAY,CAAC,WAAW,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;AACpE,CAAC","sourcesContent":["import { Platform } from 'react-native';\n\nimport type {\n AccessoryEvent,\n AccessoryEventType,\n AccessorySubscription,\n PairedAccessory,\n ShowPickerOptions,\n} from './ExpoAccessorySetup.types';\nimport NativeModule from './ExpoAccessorySetupModule';\nimport { AccessorySetupError, toAccessorySetupError } from './errors';\nimport { toNativePickerOptions } from './pickerOptions';\n\nexport * from './ExpoAccessorySetup.types';\nexport {\n AccessorySetupError,\n AccessorySetupErrorCodes,\n isAccessorySetupError,\n type AccessorySetupErrorCode,\n} from './errors';\n\nfunction requireModule() {\n if (!NativeModule || !NativeModule.isSupported()) {\n throw new AccessorySetupError(\n 'UNSUPPORTED',\n NativeModule\n ? 'Accessory setup is not supported on this device (iOS 18+ or Android with companion device setup is required).'\n : 'The ExpoAccessorySetup native module is not available. Use a development build, not Expo Go.'\n );\n }\n return NativeModule;\n}\n\nasync function call<T>(body: () => Promise<T>): Promise<T> {\n try {\n return await body();\n } catch (error) {\n throw toAccessorySetupError(error);\n }\n}\n\n/** `true` on iOS 18+ and on Android devices with the companion device setup feature. */\nexport function isSupported(): boolean {\n return NativeModule?.isSupported() ?? false;\n}\n\n/**\n * Shows the system accessory picker and resolves with the accessory the user selected.\n * Rejects with `USER_CANCELLED` when the user dismisses the picker and `ALREADY_IN_PROGRESS`\n * when another picker is showing.\n */\nexport function showPicker(options: ShowPickerOptions): Promise<PairedAccessory> {\n return call(() => {\n const module = requireModule();\n const platform = Platform.OS === 'ios' ? 'ios' : 'android';\n return module.showPicker(toNativePickerOptions(options, platform));\n });\n}\n\nexport function getPairedAccessories(): Promise<PairedAccessory[]> {\n return call(() => requireModule().getPairedAccessories());\n}\n\n/** Rejects with `NOT_FOUND` when no paired accessory has this `id`. */\nexport function removeAccessory(id: string): Promise<void> {\n return call(() => requireModule().removeAccessory(id));\n}\n\nconst nativeEventNames = {\n accessoryAdded: 'onAccessoryAdded',\n accessoryRemoved: 'onAccessoryRemoved',\n accessoryChanged: 'onAccessoryChanged',\n} as const satisfies Record<AccessoryEventType, string>;\n\n/**\n * On Android, events only fire for changes made through this module. Accessories removed from\n * system settings disappear from the next `getPairedAccessories()` result without an event.\n */\nexport function addAccessoryListener(\n type: AccessoryEventType,\n listener: (event: AccessoryEvent) => void\n): AccessorySubscription {\n if (!NativeModule) {\n return { remove() {} };\n }\n return NativeModule.addListener(nativeEventNames[type], listener);\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AccessoryPlatform, ShowPickerOptions } from './ExpoAccessorySetup.types';
|
|
2
|
+
type NativeFilter = {
|
|
3
|
+
/** Upper case. 128-bit form on Android, as given on iOS. */
|
|
4
|
+
serviceUUID?: string;
|
|
5
|
+
companyIdentifier?: number;
|
|
6
|
+
nameSubstring?: string;
|
|
7
|
+
};
|
|
8
|
+
export type IosNativePickerOptions = NativeFilter & {
|
|
9
|
+
displayName: string;
|
|
10
|
+
productImageUri: string;
|
|
11
|
+
range: 'default' | 'immediate';
|
|
12
|
+
};
|
|
13
|
+
export type AndroidNativePickerOptions = NativeFilter & {
|
|
14
|
+
singleDevice: boolean;
|
|
15
|
+
namePattern?: string;
|
|
16
|
+
};
|
|
17
|
+
export type NativePickerOptions = IosNativePickerOptions | AndroidNativePickerOptions;
|
|
18
|
+
export declare function toNativePickerOptions(options: ShowPickerOptions, platform: AccessoryPlatform): NativePickerOptions;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=pickerOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pickerOptions.d.ts","sourceRoot":"","sources":["../src/pickerOptions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAe,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpG,KAAK,YAAY,GAAG;IAClB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,SAAS,GAAG,WAAW,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG;IACtD,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG,0BAA0B,CAAC;AA4CtF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,iBAAiB,GAC1B,mBAAmB,CA6BrB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Image } from 'react-native';
|
|
2
|
+
import { AccessorySetupError } from './errors';
|
|
3
|
+
const UUID_16 = /^[0-9A-F]{4}$/;
|
|
4
|
+
const UUID_32 = /^[0-9A-F]{8}$/;
|
|
5
|
+
const UUID_128 = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/;
|
|
6
|
+
const BLUETOOTH_BASE_UUID_SUFFIX = '-0000-1000-8000-00805F9B34FB';
|
|
7
|
+
function invalid(message) {
|
|
8
|
+
throw new AccessorySetupError('INVALID_OPTIONS', message);
|
|
9
|
+
}
|
|
10
|
+
function parseServiceUUID(value, platform) {
|
|
11
|
+
const uuid = value.trim().toUpperCase();
|
|
12
|
+
if (!UUID_16.test(uuid) && !UUID_32.test(uuid) && !UUID_128.test(uuid)) {
|
|
13
|
+
invalid(`filter.serviceUUID "${value}" is not a 16-bit, 32-bit, or 128-bit UUID.`);
|
|
14
|
+
}
|
|
15
|
+
if (platform === 'ios' || UUID_128.test(uuid)) {
|
|
16
|
+
return uuid;
|
|
17
|
+
}
|
|
18
|
+
return `${uuid.padStart(8, '0')}${BLUETOOTH_BASE_UUID_SUFFIX}`;
|
|
19
|
+
}
|
|
20
|
+
function parseCompanyIdentifier(value) {
|
|
21
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
|
|
22
|
+
invalid(`filter.companyIdentifier ${value} is not an integer between 0 and 0xFFFF.`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function nonEmpty(value, field) {
|
|
27
|
+
if (value.trim().length === 0) {
|
|
28
|
+
invalid(`${field} must not be empty.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function resolveImageUri(source) {
|
|
33
|
+
const uri = typeof source === 'number' ? Image.resolveAssetSource(source)?.uri : source.uri;
|
|
34
|
+
if (!uri) {
|
|
35
|
+
invalid('ios.productImage could not be resolved to a URI.');
|
|
36
|
+
}
|
|
37
|
+
return uri;
|
|
38
|
+
}
|
|
39
|
+
export function toNativePickerOptions(options, platform) {
|
|
40
|
+
const { serviceUUID, companyIdentifier, nameSubstring } = options.filter;
|
|
41
|
+
if (serviceUUID === undefined && companyIdentifier === undefined) {
|
|
42
|
+
invalid('filter needs a serviceUUID or a companyIdentifier.');
|
|
43
|
+
}
|
|
44
|
+
const filter = {
|
|
45
|
+
serviceUUID: serviceUUID === undefined ? undefined : parseServiceUUID(serviceUUID, platform),
|
|
46
|
+
companyIdentifier: companyIdentifier === undefined ? undefined : parseCompanyIdentifier(companyIdentifier),
|
|
47
|
+
nameSubstring: nameSubstring === undefined ? undefined : nonEmpty(nameSubstring, 'filter.nameSubstring'),
|
|
48
|
+
};
|
|
49
|
+
if (platform === 'ios') {
|
|
50
|
+
return {
|
|
51
|
+
...filter,
|
|
52
|
+
displayName: nonEmpty(options.ios.displayName, 'ios.displayName'),
|
|
53
|
+
productImageUri: resolveImageUri(options.ios.productImage),
|
|
54
|
+
range: options.ios.range ?? 'default',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { singleDevice = true, namePattern } = options.android ?? {};
|
|
58
|
+
return {
|
|
59
|
+
...filter,
|
|
60
|
+
singleDevice,
|
|
61
|
+
namePattern: namePattern === undefined ? undefined : nonEmpty(namePattern, 'android.namePattern'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=pickerOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pickerOptions.js","sourceRoot":"","sources":["../src/pickerOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import { Image } from 'react-native';\n\nimport type { AccessoryPlatform, ImageSource, ShowPickerOptions } from './ExpoAccessorySetup.types';\nimport { AccessorySetupError } from './errors';\n\ntype NativeFilter = {\n /** Upper case. 128-bit form on Android, as given on iOS. */\n serviceUUID?: string;\n companyIdentifier?: number;\n nameSubstring?: string;\n};\n\nexport type IosNativePickerOptions = NativeFilter & {\n displayName: string;\n productImageUri: string;\n range: 'default' | 'immediate';\n};\n\nexport type AndroidNativePickerOptions = NativeFilter & {\n singleDevice: boolean;\n namePattern?: string;\n};\n\nexport type NativePickerOptions = IosNativePickerOptions | AndroidNativePickerOptions;\n\nconst UUID_16 = /^[0-9A-F]{4}$/;\nconst UUID_32 = /^[0-9A-F]{8}$/;\nconst UUID_128 = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/;\nconst BLUETOOTH_BASE_UUID_SUFFIX = '-0000-1000-8000-00805F9B34FB';\n\nfunction invalid(message: string): never {\n throw new AccessorySetupError('INVALID_OPTIONS', message);\n}\n\nfunction parseServiceUUID(value: string, platform: AccessoryPlatform): string {\n const uuid = value.trim().toUpperCase();\n if (!UUID_16.test(uuid) && !UUID_32.test(uuid) && !UUID_128.test(uuid)) {\n invalid(`filter.serviceUUID \"${value}\" is not a 16-bit, 32-bit, or 128-bit UUID.`);\n }\n if (platform === 'ios' || UUID_128.test(uuid)) {\n return uuid;\n }\n return `${uuid.padStart(8, '0')}${BLUETOOTH_BASE_UUID_SUFFIX}`;\n}\n\nfunction parseCompanyIdentifier(value: number): number {\n if (!Number.isInteger(value) || value < 0 || value > 0xffff) {\n invalid(`filter.companyIdentifier ${value} is not an integer between 0 and 0xFFFF.`);\n }\n return value;\n}\n\nfunction nonEmpty(value: string, field: string): string {\n if (value.trim().length === 0) {\n invalid(`${field} must not be empty.`);\n }\n return value;\n}\n\nfunction resolveImageUri(source: ImageSource): string {\n const uri = typeof source === 'number' ? Image.resolveAssetSource(source)?.uri : source.uri;\n if (!uri) {\n invalid('ios.productImage could not be resolved to a URI.');\n }\n return uri;\n}\n\nexport function toNativePickerOptions(\n options: ShowPickerOptions,\n platform: AccessoryPlatform\n): NativePickerOptions {\n const { serviceUUID, companyIdentifier, nameSubstring } = options.filter;\n if (serviceUUID === undefined && companyIdentifier === undefined) {\n invalid('filter needs a serviceUUID or a companyIdentifier.');\n }\n const filter: NativeFilter = {\n serviceUUID: serviceUUID === undefined ? undefined : parseServiceUUID(serviceUUID, platform),\n companyIdentifier:\n companyIdentifier === undefined ? undefined : parseCompanyIdentifier(companyIdentifier),\n nameSubstring:\n nameSubstring === undefined ? undefined : nonEmpty(nameSubstring, 'filter.nameSubstring'),\n };\n\n if (platform === 'ios') {\n return {\n ...filter,\n displayName: nonEmpty(options.ios.displayName, 'ios.displayName'),\n productImageUri: resolveImageUri(options.ios.productImage),\n range: options.ios.range ?? 'default',\n };\n }\n\n const { singleDevice = true, namePattern } = options.android ?? {};\n return {\n ...filter,\n singleDevice,\n namePattern:\n namePattern === undefined ? undefined : nonEmpty(namePattern, 'android.namePattern'),\n };\n}\n"]}
|