├── .gitignore ├── .versions ├── LICENSE ├── README.md ├── lib └── reactive-ibeacons.js ├── package.js └── tests └── client ├── reactive-ibeacons-tests.js └── stubs.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 2 | 3 | *.iml 4 | 5 | ## Directory-based project format: 6 | .idea/ 7 | 8 | .project -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | callback-hook@1.0.4 9 | check@1.1.0 10 | ddp@1.2.2 11 | ddp-client@1.2.1 12 | ddp-common@1.2.2 13 | ddp-server@1.2.2 14 | deps@1.0.9 15 | diff-sequence@1.0.1 16 | ecmascript@0.1.6 17 | ecmascript-runtime@0.2.6 18 | ejson@1.0.7 19 | geojson-utils@1.0.4 20 | html-tools@1.0.5 21 | htmljs@1.0.5 22 | id-map@1.0.4 23 | jquery@1.11.4 24 | local-test:mbanting:reactive-ibeacons@1.2.0 25 | logging@1.0.8 26 | mbanting:reactive-ibeacons@1.2.0 27 | meteor@1.1.10 28 | minimongo@1.0.10 29 | mongo@1.1.3 30 | mongo-id@1.0.1 31 | npm-mongo@1.4.39_1 32 | observe-sequence@1.0.7 33 | ordered-dict@1.0.4 34 | promise@0.5.1 35 | random@1.0.5 36 | reactive-var@1.0.6 37 | retry@1.0.4 38 | routepolicy@1.0.6 39 | spacebars@1.0.7 40 | spacebars-compiler@1.0.7 41 | tinytest@1.0.6 42 | tracker@1.0.9 43 | ui@1.0.8 44 | underscore@1.0.4 45 | webapp@1.2.3 46 | webapp-hashing@1.0.5 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mbanting:reactive-ibeacons 2 | 3 | Turns [Apple iBeacons](https://developer.apple.com/ibeacon/) into reactive data sources in your Meteor Cordova apps. 4 | 5 | ## Installation 6 | 7 | ``` 8 | meteor add mbanting:reactive-ibeacons 9 | ``` 10 | 11 | ## Description 12 | 13 | Hardware can be reactive data sources too! 14 | 15 | iBeacons are Bluetooth Low Energy (BLE) hardware devices that enable new location awareness [possibilities](http://blog.mowowstudios.com/2015/02/100-use-cases-examples-ibeacon-technology/) for apps. They can be used to establish a region around an object or location, allowing your Meteor Cordova app to determine when it has entered or left the region (also known as Monitoring), along with an estimation of proximity to a beacon (also known as Ranging). This package turns iBeacons into reactive data sources in your Meteor Cordova app, providing an easy way for you to handle these proximity-related events. 16 | 17 | 18 | ## Compatibility 19 | This package builds on top Peter Metz's [Cordova iBeacon plugin](https://github.com/petermetz/cordova-plugin-ibeacon), and is therefore compatible with iOS 7+ (using its Core Location framework) and Android (using the [AltBeacon's](http://altbeacon.org/) Android implementation). 20 | 21 | ## Usage 22 | iBeacons broadcast self-contained packets of data in set intervals for detection by your app, allowing your app to know when a user is in the vicinity of a beacon. To understand how to use this package, you need to have some basic understanding of how iBeacons work. 23 | 24 | ### iBeacon Basics 25 | iBeacons regularly broadcast a signal for your app to detect. Included in this signal is the identifier of the iBeacon, and additional proximity information. 26 | 27 | Every iBeacon is designated and broadcasts an identifier composed of 28 | - `uuid`: 16 byte identifier, usually expressed as a series of hexadecimal digits separated by dashes, used to differentiate a large group of related beacons. 29 | - `major`: Integer between 0 and 65535, usually used to group a subset of the larger group. 30 | - `minor`: Integer between 0 and 65535, usually used to identify an individual beacon 31 | 32 | How you organize and designate these values for your iBeacons is up to you. One suggested approach is to set the `UUID` to the same value for all iBeacons that you want your app to detect. This is because apps can't just detect every iBeacon that is out there. You need to specify which iBeacons it should pick up by specifying the beacon region. 33 | 34 | ### Beacon Region 35 | The first step is to instantiate a `ReactiveBeaconRegion` object by specifying the beacon region, effectively telling your app what beacons to detect. This includes an arbitrary `identifier` label value and the `uuid` (in a string form of 32 hexadecimal digits, split into 5 groups, separated by dashes). This will allow your app to detect all iBeacons with the specified `uuid`, regardless of its `major` or `minor` value. 36 | ``` 37 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier: "beacons on shelf", uuid: "F7826DA1-4FA2-4E97-8022-BC5B71E0893A"}); 38 | ``` 39 | You can optionally specify the `major` value if you want to narrow your beacon region further to a smaller subset of beacons. You can even specify the `minor` value to narrow it to a single beacon altogether. 40 | ``` 41 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier: "beacons on shelf", uuid: "F7826DA1-4FA2-4E97-8022-BC5B71E0893A", "major":5, "minor":26}); 42 | ``` 43 | 44 | ### Beacon Data 45 | Once you've instantiated your `ReactiveBeaconRegion` you can make the following call to get the proximity information for the beacon region. 46 | 47 | ``` 48 | var beaconRegion = reactiveBeaconRegion.getBeaconRegion(); 49 | ``` 50 | 51 | If beacons are detected, this function returns a data structure similar to the following: 52 | ``` 53 | { 54 | "identifier": "beacons on shelf", 55 | "uuid": "F7826DA1-4FA2-4E97-8022-BC5B71E0893A", 56 | "beacons": [ 57 | { 58 | "minor": 25, 59 | "rssi": -65, 60 | "major": 5, 61 | "proximity": "ProximityImmediate", 62 | "accuracy": 0.10, 63 | "uuid": "F7826DA1-4FA2-4E97-8022-BC5B71E0893A" 64 | }, 65 | { 66 | "minor": 26, 67 | "rssi": -65, 68 | "major": 5, 69 | "proximity": "ProximityNear", 70 | "accuracy": 0.12, 71 | "uuid": "F7826DA1-4FA2-4E97-8022-BC5B71E0893A" 72 | } 73 | ], 74 | "inRegion": true 75 | } 76 | ``` 77 | The properties are: 78 | - `identifier`: The identifier you used when creating this `ReactiveBeaconRegion` 79 | - `uuid`: The `uuid` of the beacons being detected by this `ReactiveBeaconRegion` 80 | - `major`: If passed in when instantiating the `ReactiveBeaconRegion`, the `major` id of the beacons being detected 81 | - `minor`: If passed in when instantiating the `ReactiveBeaconRegion`, the `minor` id of the beacons being detected 82 | - `beacons`: An array of beacon information for all currently ranged beacons. Ordered with the closest beacon at the beginning of the array 83 | - `uuid`: The UUID of the beacon 84 | - `major`: The major ID of the beacon 85 | - `minor`: The minor ID of the beacon 86 | - `proximity`: The relative distance to the beacon, one of "ProximityImmediate", "ProximityNear", "ProximityFar" 87 | - `accuracy`: The accuracy of the proximity value, measured in meters from the beacon. 88 | - `rssi`: The received signal strength of the beacon, measured in decibels. 89 | - `inRegion`: true if device crossed into the monitored region, false if the deviced crossed outside the monitored region, null if unknown 90 | 91 | ### Reactivity 92 | Being a reactive data source, you can use this reactively and respond appropriately to proximity changes. 93 | ``` 94 | Tracker.autorun(function () { 95 | if (reactiveBeaconRegion.getBeaconRegion().inRegion) { 96 | // popup message welcoming user to the neighborhood! 97 | } 98 | }); 99 | ``` 100 | 101 | ## Permissions 102 | Having your app detect iBeacons requires it to have access to your user's location. Creating a `ReactiveBeaconRegion` will trigger your app to request for this permission, if it doesn't have this privilege already. 103 | 104 | ## Background Monitoring 105 | As mentioned above, detecting and gathering iBeacon data is done via a combination of _monitoring_ and _ranging_. Monitoring a region enables your app to know when a device enters or exits the range of beacons defined by the region, updating the `inRegion` property. Ranging is more granular. It updates the list of beacons and their information in the `beacons` array. Ranging works only when the user is actively using your application (the app is in the foreground). However, monitoring works even if the app is asleep in the background. iOS and Android will wake up your app and give it a short amount of time (5-10 seconds) to handle the event with code that doesn't require a UI (for example updating application state, calling a web service, or sending a local notification). 106 | 107 | ## Advertising 108 | You can also have the device advertise itself as a beacon. Currently advertising is only supported on iOS. To check whether your device supports advertising: 109 | ``` 110 | reactiveBeaconRegion.canAdvertise( function(result) { 111 | if (result) console.log("Hot dog, we can advertise!"); 112 | }); 113 | ``` 114 | 115 | To find out if you're already advertising: 116 | ``` 117 | reactiveBeaconRegion.isAdvertising( function(result) { 118 | if (result) console.log("Advertising now."); 119 | }); 120 | ``` 121 | 122 | To start advertising: 123 | ``` 124 | reactiveBeaconRegion.startAdvertising( 125 | "4493DE05-A461-406E-9CC0-C3EEF370C94F", //uuid 126 | "Liam's Beacon", //identifier 127 | 1000, //major 128 | 2000, //minor 129 | // callback called when advertising actually starts 130 | function(pluginResult) { 131 | console.log(JSON.stringify(pluginResult.region)); 132 | }, 133 | // callback called when device state changes 134 | function(pluginResult) { 135 | console.log(pluginResult.state); 136 | } 137 | ); 138 | ``` 139 | 140 | To stop advertising: 141 | ``` 142 | advertiser.stopAdvertising(); 143 | ``` 144 | 145 | 146 | ## Limitations 147 | - As with any functionality relying on Cordova, this will only work after Meteor has started. You can wrap your `ReactiveBeaconRegion` constructor call in a `Meteor.startup()` function to make sure this happens. 148 | - iOS actually allows you to define up to 20 different regions. Unfortunately, due to a [limitation](https://github.com/petermetz/cordova-plugin-ibeacon/issues/166) with the underlying plugin, only one beacon region can be monitored and ranged at any given time. Possible workarounds are under investigation. 149 | 150 | ## Testing 151 | 152 | This package has been tested extensively on iOS, and on a limited basis on Android. Any help testing on Android is much appreciated! 153 | 154 | To run this package's unit tests (implemented with TinyTest), type the following: 155 | 156 | ``` 157 | meteor test-packages ./ 158 | ``` 159 | ## Contributions 160 | - [@mrlowe](https://github.com/mrlowe): added Advertising functionality 161 | 162 | ## Feedback 163 | If you have any problems, questions, or have general feedback, please feel free to contact me! 164 | 165 | ## License 166 | The code for this package is licensed under the [MIT License](http://opensource.org/licenses/MIT). 167 | -------------------------------------------------------------------------------- /lib/reactive-ibeacons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This instantiates the ReactiveBeaconRegion instance, a reactive data source for beacon information for the currently 3 | * monitored / ranged beacon region. 4 | * 5 | * @param beaconRegion an object describing the beacon region to monitor/range, with properties identifier, uuid, major, and minor 6 | * @param disableMonitoring true if monitoring is to be disabled; optional defaults to false 7 | * @param disableRanging true if ranging is to be disabled; optional defaults to false 8 | * @constructor 9 | */ 10 | ReactiveBeaconRegion = function(beaconRegion, disableMonitoring, disableRanging, disableConsoleLog) { 11 | 12 | // check the provided beaconRegion 13 | check(beaconRegion, { 14 | identifier: String, 15 | uuid: String, 16 | // Optional, but if present must be number. 17 | // Checks for no major/minor property, or value is undefined, null, or Number 18 | major: Match.Optional(Match.OneOf(undefined, null, Number)), 19 | minor: Match.Optional(Match.OneOf(undefined, null, Number)) 20 | }); 21 | 22 | check(disableMonitoring, Match.Optional(Boolean)); 23 | check(disableRanging, Match.Optional(Boolean)); 24 | 25 | // create a new Dependency 26 | this.dep = new Tracker.Dependency(); 27 | 28 | // default disabling of monitoring and ranging to false 29 | if (disableMonitoring == null) { 30 | disableMonitoring = false; 31 | } 32 | 33 | if (disableRanging == null) { 34 | disableRanging = false; 35 | } 36 | 37 | // create this object's reactive beaconRegion and collection of beacons being monitored/ranged 38 | this.beaconRegion = beaconRegion; 39 | this.beaconRegion.beacons = []; 40 | this.beaconRegion.inRegion = null; 41 | this.logToConsole = !disableConsoleLog; 42 | 43 | // turn off logging 44 | cordova.plugins.locationManager.disableDebugLogs(); 45 | 46 | // setup beacon monitoring and ranging 47 | this.delegate = new cordova.plugins.locationManager.Delegate(); 48 | var self = this; 49 | 50 | // manage plugin logs 51 | this.log = function() { 52 | if(self.logToConsole) { 53 | console.log.apply(console, arguments); 54 | } 55 | } 56 | 57 | // callback to determine new monitoring state based on CLRegionState value 58 | this.delegate.didDetermineStateForRegion = function (pluginResult) { 59 | var newState = null; 60 | if (pluginResult.state === "CLRegionStateInside") { 61 | newState = true; 62 | } else if (pluginResult.state === "CLRegionStateOutside") { 63 | newState = false; 64 | } else if (pluginResult.state === "CLRegionStateUnknown") { 65 | newState = null; 66 | } 67 | 68 | // if new state, then invalidate dependencies 69 | if (self.beaconRegion.inRegion != newState) { 70 | self.beaconRegion.inRegion = newState; 71 | self.dep.changed(); 72 | } 73 | }; 74 | 75 | this.delegate.didStartMonitoringForRegion = function (pluginResult) { 76 | self.log('didStartMonitoringForRegion: ' + JSON.stringify(pluginResult)); 77 | }; 78 | 79 | this.delegate.didRangeBeaconsInRegion = function (pluginResult) { 80 | var newBeacons = []; 81 | for (var i = 0; i < pluginResult.beacons.length; i++) 82 | { 83 | // Insert beacon into table of found beacons. 84 | var newBeacon = pluginResult.beacons[i]; 85 | 86 | // Android gives us strings instead of ints, I think because the cordova 87 | // plugin uses AltBeacon. Fix that here: 88 | if(newBeacon.major && ( typeof newBeacon.major === 'string')) { 89 | newBeacon.major = parseInt(newBeacon.major); 90 | } 91 | if(newBeacon.minor && ( typeof newBeacon.minor === 'string')) { 92 | newBeacon.minor = parseInt(newBeacon.minor); 93 | } 94 | 95 | newBeacons.push(newBeacon); 96 | } 97 | 98 | // determine if there's a difference and if so update and invalidate dependencies 99 | if (self._areBeaconsUpdated(newBeacons)) { 100 | self.beaconRegion.beacons = newBeacons; 101 | self.dep.changed(); 102 | } 103 | }; 104 | 105 | var targetBeaconRegion = new cordova.plugins.locationManager.BeaconRegion(beaconRegion.identifier, beaconRegion.uuid, beaconRegion.major, beaconRegion.minor, true); 106 | 107 | cordova.plugins.locationManager.setDelegate(this.delegate); 108 | 109 | // required in iOS 8+ 110 | if (disableMonitoring) { 111 | cordova.plugins.locationManager.requestWhenInUseAuthorization(); 112 | } else { 113 | // need location services to always be enabled for monitoring support 114 | cordova.plugins.locationManager.requestAlwaysAuthorization(); 115 | } 116 | 117 | // Start ranging. 118 | if (!disableRanging) { 119 | self.log('starting ranging ' + JSON.stringify(targetBeaconRegion)); 120 | cordova.plugins.locationManager.startRangingBeaconsInRegion(targetBeaconRegion) 121 | .fail(console.error) 122 | .done(); 123 | } 124 | 125 | // start monitoring 126 | if (!disableMonitoring) { 127 | self.log('starting monitoring ' + JSON.stringify(targetBeaconRegion)); 128 | cordova.plugins.locationManager.startMonitoringForRegion(targetBeaconRegion) 129 | .fail(console.error) 130 | .done(); 131 | } 132 | }; 133 | 134 | /** 135 | * This function returns the current beacons being monitored / ranged. 136 | * @returns {Array} 137 | */ 138 | ReactiveBeaconRegion.prototype.getBeaconRegion = function() { 139 | this.dep.depend(); 140 | this.log('getbeaconregion ' + JSON.stringify(this.beaconRegion)); 141 | return this.beaconRegion; 142 | }; 143 | 144 | /** 145 | * This method determines if the given beacons are different than the current beacons 146 | * @param newBeacons 147 | * @private 148 | */ 149 | ReactiveBeaconRegion.prototype._areBeaconsUpdated = function(newBeacons) { 150 | return this.beaconRegion.beacons.length != newBeacons.length || 151 | _.uniq(this.beaconRegion.beacons.concat(newBeacons), false, 152 | function(beacon){return JSON.stringify(beacon)}).length != this.beaconRegion.beacons.length; 153 | }; 154 | 155 | /** 156 | * Simple Boolean Callback used when asking about something's status 157 | * @callback ReactiveBeaconRegion~onBooleanResult 158 | * @param {Boolean} booleanValue 159 | */ 160 | 161 | /** 162 | * Find out whether the device can advertise 163 | * @param {ReactiveBeaconRegion~onBooleanResult} callback - called to deliver the boolean value 164 | */ 165 | ReactiveBeaconRegion.prototype.canAdvertise= function(callback) { 166 | 167 | cordova.plugins.locationManager.isAdvertisingAvailable() 168 | .then(function(isSupported){ 169 | callback(isSupported); 170 | }) 171 | .fail(console.error) 172 | .done(); 173 | }; 174 | 175 | 176 | /** 177 | * Find out whether the device is currently advertising 178 | * @param {ReactiveBeaconRegion~onBooleanResult} callback - called to deliver the boolean value 179 | */ 180 | ReactiveBeaconRegion.prototype.isAdvertising= function(callback) { 181 | 182 | cordova.plugins.locationManager.isAdvertising() 183 | .then(function(isAdvertising){ 184 | callback(isAdvertising); 185 | }) 186 | .fail(console.error) 187 | .done(); 188 | }; 189 | 190 | /** 191 | * Callback used when handling pluginResult events from Cordova 192 | * @callback ReactiveBeaconRegion~callbackPluginResult 193 | * @param {Object} pluginResult 194 | */ 195 | 196 | /** 197 | * Begin advertising with a certain UUID, major and minor 198 | * @param {String} uuid - the unique identifier for the beacon 199 | * @param {Number} major - the major int 200 | * @param {Number} minor - the minor int 201 | * @param {ReactiveBeaconRegion~callbackPluginResult} onStarted - callback that reports when the beacon starts advertising 202 | * @param {ReactiveBeaconRegion~callbackPluginResult} onStateChanged - callback that reports when the beacon state changes 203 | */ 204 | ReactiveBeaconRegion.prototype.startAdvertising= function(uuid, identifier, major, minor, onStarted, onStateChanged) { 205 | var beaconRegion = new cordova.plugins.locationManager.BeaconRegion(identifier, uuid, major, minor); 206 | 207 | // Event when advertising starts (there may be a short delay after the request) 208 | // The property 'region' provides details of the broadcasting Beacon 209 | this.delegate.peripheralManagerDidStartAdvertising = onStarted; 210 | 211 | // Event when bluetooth transmission state changes 212 | // If 'state' is not set to BluetoothManagerStatePoweredOn when advertising cannot start 213 | this.delegate.peripheralManagerDidUpdateState = onStateChanged; 214 | 215 | cordova.plugins.locationManager.setDelegate(this.delegate); 216 | 217 | // Verify the platform supports transmitting as a beacon 218 | var self = this; 219 | cordova.plugins.locationManager.isAdvertisingAvailable() 220 | .then(function(isSupported){ 221 | if (isSupported) { 222 | cordova.plugins.locationManager.startAdvertising(beaconRegion) 223 | .fail(console.error) 224 | .done(); 225 | } else { 226 | self.log("Advertising not supported"); 227 | } 228 | }) 229 | .fail(console.error) 230 | .done(); 231 | } 232 | 233 | /** 234 | * Stop the device from advertising 235 | */ 236 | ReactiveBeaconRegion.prototype.stopAdvertising= function() { 237 | cordova.plugins.locationManager.stopAdvertising() 238 | .fail(console.error) 239 | .done(); 240 | } 241 | 242 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'mbanting:reactive-ibeacons', 3 | summary: "Turns iBeacons into reactive data sources in your Meteor Cordova apps.", 4 | version: '1.2.1', 5 | git: "https://github.com/mbanting/meteor-reactive-ibeacons" 6 | }); 7 | 8 | 9 | /** 10 | * Cordova plugin dependencies 11 | */ 12 | Cordova.depends({ 13 | 'com.unarin.cordova.beacon': '3.4.0' 14 | }); 15 | 16 | Package.onUse(function(api) { 17 | api.versionsFrom('1.1.0.3'); 18 | api.use(['check', 'tracker', 'underscore']); 19 | api.addFiles('lib/reactive-ibeacons.js', ["web.cordova"]); 20 | api.export('ReactiveBeaconRegion', ['web.cordova']); 21 | 22 | }); 23 | 24 | Package.onTest(function(api) { 25 | api.use(['check', 'tracker', 'underscore']); 26 | api.use(['tinytest', 'mbanting:reactive-ibeacons']); 27 | api.addFiles(['tests/client/stubs.js', 'lib/reactive-ibeacons.js'], ["client"]); // tests can run on client, make files available 28 | api.addFiles('tests/client/reactive-ibeacons-tests.js', 'client'); 29 | api.export('ReactiveBeaconRegion', 'client'); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/client/reactive-ibeacons-tests.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test to ensure ReactiveBeaconRegion is accessible 4 | */ 5 | Tinytest.add('ReactiveBeaconRegion exists', function (test) { 6 | test.isNotUndefined(ReactiveBeaconRegion, "Expected ReactiveBeaconRegion to be defined") 7 | }); 8 | 9 | /** 10 | * Test to ensure ReactiveBeaconRegion accepts a valid beaconRegion and disable flags 11 | */ 12 | Tinytest.add('new ReactiveBeaconRegion()', function (test) { 13 | test.throws(function(){new ReactiveBeaconRegion()}); 14 | test.throws(function(){new ReactiveBeaconRegion({identifier:1, uuid:"123"})}); 15 | test.throws(function(){new ReactiveBeaconRegion({identifier:"123", uuid:1})}); 16 | test.throws(function(){new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:1, minor:"123"})}); 17 | test.throws(function(){new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:"123", minor:1})}); 18 | test.instanceOf(new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:123, minor:123}), ReactiveBeaconRegion); 19 | test.throws(function(){new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:123, minor:123}, 1, true)}); 20 | test.throws(function(){new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:123, minor:123}, true, 1)}); 21 | test.instanceOf(new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:12, minor:23}, true, true), ReactiveBeaconRegion); 22 | test.instanceOf(new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:12, minor:23}, false, true), ReactiveBeaconRegion); 23 | test.instanceOf(new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:12, minor:23}, true, false), ReactiveBeaconRegion); 24 | test.instanceOf(new ReactiveBeaconRegion({identifier:"123", uuid:"123", major:16, minor:23}, false, false), ReactiveBeaconRegion); 25 | }); 26 | 27 | // TODO: Test to ensure disable flags don't start ranging / monitoring 28 | // Need spy support or do it via the locationManager stub 29 | 30 | /** 31 | * Test areBeaconsUpdated detecting changes in beacon information 32 | */ 33 | Tinytest.add('ReactiveBeaconRegion._areBeaconsUpdated()', function (test) { 34 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 35 | 36 | var existingBeacons = [ 37 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 38 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityNear", "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} 39 | ]; 40 | 41 | // first test with same beacons 42 | reactiveBeaconRegion.beaconRegion.beacons = existingBeacons; 43 | test.isFalse(reactiveBeaconRegion._areBeaconsUpdated(existingBeacons), "Expected beacon comparison to detect same beacons"); 44 | 45 | // now test with change to existing beacons and with an additional beacon 46 | var differentBeacons = [ 47 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 48 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityFar" /* changed */, 49 | "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} 50 | ]; 51 | var additionalBeacons = [ 52 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 53 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityNear", "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 54 | {"minor": 13913, "rssi": -66, "major": 22728, "proximity": "ProximityNear", "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} // added 55 | ]; 56 | reactiveBeaconRegion.beaconRegion.beacons = existingBeacons; 57 | test.isTrue(reactiveBeaconRegion._areBeaconsUpdated(differentBeacons), "Expected beacon comparison to detect different beacons"); 58 | test.isTrue(reactiveBeaconRegion._areBeaconsUpdated(additionalBeacons), "Expected beacon comparison to the new beacon"); 59 | }); 60 | 61 | 62 | /** 63 | * Test reactivity on updated beacon monitoring 64 | */ 65 | Tinytest.add('ReactiveBeaconRegion.delegate.didDetermineStateForRegion()', function (test) { 66 | var inRegion = false; 67 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 68 | var computationRanAgain = false; 69 | 70 | Tracker.autorun(function(computation) { 71 | var beaconRegion = reactiveBeaconRegion.getBeaconRegion(); 72 | inRegion = beaconRegion.inRegion; 73 | if (!computation.firstRun) { 74 | computationRanAgain = true; 75 | } 76 | }) 77 | 78 | test.isNull(inRegion, "Expected inRegion to be null on initial instanciation of ReactiveBeaconRegion"); 79 | 80 | // simulate in region of beacon region 81 | cordova.plugins.locationManager.getDelegate().didDetermineStateForRegion( { 82 | state: "CLRegionStateInside" 83 | }); 84 | Tracker.flush(); 85 | test.isTrue(inRegion, "Expected inRegion to be true on CLRegionStateInside state"); 86 | 87 | // simulate out of region of beacon region 88 | cordova.plugins.locationManager.getDelegate().didDetermineStateForRegion( { 89 | state: "CLRegionStateOutside" 90 | }); 91 | Tracker.flush(); 92 | test.isFalse(inRegion, "Expected inRegion to be false on CLRegionStateOutside state"); 93 | 94 | // simulate same beacon region 95 | computationRanAgain = false; 96 | cordova.plugins.locationManager.getDelegate().didDetermineStateForRegion( { 97 | state: "CLRegionStateOutside" 98 | }); 99 | Tracker.flush(); 100 | test.isFalse(computationRanAgain, "Expected computation to not run again on same beacon monitoring information"); 101 | 102 | // simulate unknown beacon region state 103 | cordova.plugins.locationManager.getDelegate().didDetermineStateForRegion( { 104 | state: "invalid" 105 | }); 106 | Tracker.flush(); 107 | test.isNull(inRegion, "Expected inRegion to be null on invalid monitor state"); 108 | }); 109 | 110 | /** 111 | * Test reactivity on updated beacon ranging 112 | */ 113 | Tinytest.add('ReactiveBeaconRegion.delegate.didRangeBeaconsInRegion()', function (test) { 114 | var computationRanAgain = false; 115 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 116 | Tracker.autorun(function(computation) { 117 | var beaconRegion = reactiveBeaconRegion.getBeaconRegion(); 118 | if (!computation.firstRun) { 119 | computationRanAgain = true; 120 | } 121 | }); 122 | 123 | // simulate beacons being detected 124 | cordova.plugins.locationManager.getDelegate().didRangeBeaconsInRegion( { 125 | beacons: [ 126 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 127 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityNear", "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} 128 | ] 129 | }); 130 | Tracker.flush(); 131 | test.isTrue(computationRanAgain, "Expected computation to run on initial beacon ranging information"); 132 | 133 | // setup and simulate next update 134 | computationRanAgain = false; 135 | cordova.plugins.locationManager.getDelegate().didRangeBeaconsInRegion( { 136 | beacons: [ 137 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 138 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityFar" /* changed! */, "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} 139 | ] 140 | }); 141 | Tracker.flush(); 142 | test.isTrue(computationRanAgain, "Expected computation to run on beacon ranging update"); 143 | 144 | // setup and simulate but with no update to ranging information 145 | computationRanAgain = false; 146 | cordova.plugins.locationManager.getDelegate().didRangeBeaconsInRegion( { 147 | beacons: [ 148 | {"minor": 13911, "rssi": -65, "major": 22728, "proximity": "ProximityImmediate", "accuracy": 0.12, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"}, 149 | {"minor": 13912, "rssi": -66, "major": 22728, "proximity": "ProximityFar", "accuracy": 0.11, "uuid": "F7826DA6-4FA2-4E98-8024-BC5B71E0893E"} 150 | ] 151 | }); 152 | Tracker.flush(); 153 | test.isFalse(computationRanAgain, "Expected computation to not run again on same beacon ranging information"); 154 | 155 | }); 156 | 157 | /** 158 | * Test to ensure we get the right response if the plugin can advertise 159 | */ 160 | Tinytest.add('ReactiveBeaconRegion canAdvertise', function(test) { 161 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 162 | reactiveBeaconRegion.canAdvertise( function(result) { 163 | test.isFalse(result); 164 | }); 165 | cordova.plugins.locationManager.canAdvertiseStub = true; 166 | reactiveBeaconRegion.canAdvertise( function(result) { 167 | test.isTrue(result); 168 | }); 169 | }); 170 | 171 | 172 | /** 173 | * Test to ensure we get the right response if the plugin is advertising 174 | */ 175 | Tinytest.add('ReactiveBeaconRegion isAdvertising', function(test) { 176 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 177 | reactiveBeaconRegion.isAdvertising( function(result) { 178 | test.isFalse(result); 179 | }); 180 | cordova.plugins.locationManager.isAdvertisingStub = true; 181 | reactiveBeaconRegion.isAdvertising( function(result) { 182 | test.isTrue(result); 183 | }); 184 | }); 185 | 186 | 187 | /** 188 | * Start advertising and ensure that the callback tells us we're advertising 189 | */ 190 | Tinytest.add('ReactiveBeaconRegion startAdvertising', function(test) { 191 | cordova.plugins.locationManager.isAdvertisingStub = false; 192 | cordova.plugins.locationManager.canAdvertiseStub = true; 193 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 194 | reactiveBeaconRegion.startAdvertising( 195 | "4493DE05-A461-406E-9CC0-C3EEF370C94F", //uuid 196 | "Liam's Beacon", //identifier 197 | 1000, //major 198 | 2000, //minor 199 | // we're not testing the actual data structure returned by the plugin here, 200 | // just whether we get the same values we put in 201 | function(pluginResult) { 202 | test.isNotNull(pluginResult); 203 | test.isNotNull(pluginResult.region); 204 | test.equal(pluginResult.region.uuid, "4493DE05-A461-406E-9CC0-C3EEF370C94F"); 205 | test.equal(pluginResult.region.major, 1000); 206 | test.equal(pluginResult.region.minor, 2000); 207 | }, 208 | function(pluginResult) {} 209 | ); 210 | reactiveBeaconRegion.isAdvertising( function(result) { 211 | test.isTrue(result); 212 | }); 213 | }); 214 | 215 | 216 | /** 217 | * Try to start advertising when it's not supported and fail 218 | */ 219 | Tinytest.add('ReactiveBeaconRegion failAdvertising', function(test) { 220 | cordova.plugins.locationManager.isAdvertisingStub = false; 221 | cordova.plugins.locationManager.canAdvertiseStub = false; 222 | var reactiveBeaconRegion = new ReactiveBeaconRegion({identifier:"123", uuid:"123"}); 223 | reactiveBeaconRegion.startAdvertising( 224 | "4493DE05-A461-406E-9CC0-C3EEF370C94F", //uuid 225 | "Liam's Beacon", //identifier 226 | 1000, //major 227 | 2000, //minor 228 | function(pluginResult) {}, 229 | function(pluginResult) {} 230 | ); 231 | reactiveBeaconRegion.isAdvertising( function(result) { 232 | test.isFalse(result); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /tests/client/stubs.js: -------------------------------------------------------------------------------- 1 | 2 | // stub out cordova and plugin 3 | cordova = { 4 | plugins: { 5 | locationManager: { 6 | disableDebugLogs: function(){}, 7 | BeaconRegion: function(identifier, uuid, major, minor) { 8 | return { 9 | "uuid": uuid, 10 | "major": major, 11 | "minor": minor 12 | }; 13 | }, 14 | setDelegate: function(delegate) { 15 | this.delegate = delegate; 16 | }, 17 | getDelegate: function() { 18 | return this.delegate; 19 | }, 20 | requestAlwaysAuthorization: function(){}, 21 | requestWhenInUseAuthorization: function(){}, 22 | startRangingBeaconsInRegion: function() { 23 | return { 24 | fail: function() { 25 | return { 26 | done: function() {} 27 | } 28 | } 29 | } 30 | }, 31 | startMonitoringForRegion: function() { 32 | return { 33 | fail: function() { 34 | return { 35 | done: function() {} 36 | } 37 | } 38 | } 39 | }, 40 | Delegate: function() { 41 | return {}; 42 | }, 43 | canAdvertiseStub: false, 44 | isAdvertisingAvailable: function() { 45 | const localStub = this.canAdvertiseStub; 46 | return { 47 | then: function(callback) { 48 | callback(localStub); 49 | return { 50 | fail: function() { 51 | return { 52 | done: function() {} 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | isAdvertisingStub: false, 60 | isAdvertising: function() { 61 | const localStub = this.isAdvertisingStub; 62 | return { 63 | then: function(callback) { 64 | callback(localStub); 65 | return { 66 | fail: function() { 67 | return { 68 | done: function() {} 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | startAdvertising: function(beaconRegion) { 76 | const pluginResult = { 77 | region: { 78 | uuid: beaconRegion.uuid, 79 | major: beaconRegion.major, 80 | minor: beaconRegion.minor 81 | } 82 | }; 83 | this.isAdvertisingStub = true; 84 | this.delegate.peripheralManagerDidStartAdvertising(pluginResult); 85 | return { 86 | fail: function() { 87 | return { 88 | done: function() {} 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }; 96 | --------------------------------------------------------------------------------