Wow Your Neighbors with a Beacon-Powered Easter Egg or Afikoman Hunt

It’s Spring! The flowers are blooming, the garden is defrosting, the birds are singing, and it’s almost time for Easter, Passover, or other Spring holidays that celebrate rebirth and renewal. Even in snowy Boston, where I live, the snowdrifts are slowly disappearing. What better time to organize an Easter egg hunt or Spring scavenger hunt with your family or the neighborhood kids? And what better way to show off your technical prowess than by hosting a hunt powered by beacons? Because as we know, the kids aren’t impressed nowadays with just putting chocolates in little plastic eggs – now we need to step up our game and host something more high tech.

Estimote beacons, in pretty Spring colors, are perfect candidates for this sort of use case. These little beacons are one of the prized tools in my IoT toolkit. They look like little pebbles and can be stuck on a wall or tucked anywhere, sending out a bluetooth signal that can be picked up by any bluetooth-enabled phone via a listening mobile app.

beacons2.png
I got a little excited when they came in the mail. I’ve ordered Estimote Stickers and am eagerly awaiting their arrival as well.

In this article, I’m going to walk through how I built a beacon-powered scavenger hunt type app using Telerik AppBuilder with the Kendo UI Mobile framework, two Cordova plugins, and Telerik Backend Services for push notifications.

You can download Beacon Hunt for iOS here. The source code for this article is available on GitHub.

If you want to learn more when you’re done, I’ve written about developing with Beacons previously on this site.

Design Phase

The basicconcept for this app is to leverage hidden Estimote beacons to create a scavenger hunt.

First, I decided to use push notifications as a way to geofence the members of a given hunt. Using Telerik Backend Services, we have the capability of creating ‘segments’ or groups of users who, in this case, are registered for a given scavenger hunt.

Getting up and running with push notifications is notoriously difficult, especially for the iOS developer. I was helped immensely by the sample app in the Telerik plugins marketplace and the accompanying documentation.

ss1_sm

Second, I used Kendo UI Mobile as the framework for the app’s code structure. In particular, I wanted nice-looking forms for the two initial screens of the app: the ‘sign up’ screen where a hunt organizer creates a six-digit code for the hunt (this code becomes the push notification segment); and the ‘sign in’ screen where users join a hunt and thereby sign in to a push channel.

ss2_sm

Third, I used the Social Sharing plugin from the Verified Plugins Marketplace to create an easy way for a hunt organizer to email an invitation with the hunt’s six-digit code.

ss3_sm

Finally, I used the Cordova Estimote plugin, also from the Verified Plugins Marketplace, which provides an easy way to pick up bluetooth signals from a beacon, detect its distance and determine its color (there are three colors in the pack, mint, blueberry, and ice). When the user gets within a meter of the beacon, a button appears that the hunter is prompted to press, at which time a push notification is sent out to the hunting group (i.e. the push notification channel).

ss4_sm

Ready, Set, Build!

I’ll walk you through the steps required to build this app.

Step 1: Configure Backend Services

Sign in to your Telerik Platform account and create a new app that is integrated with the Telerik Backend Services. Make sure that push notifications and cloud data services are enabled.

ScreenshotEggHunt5

Create a content type called “Hunt”. This is where the six-digit PIN for a hunt will be stored. Add a column to this type called HuntPIN with type text:

ScreenshotEggHunt2

As unique hunt PINs are created, this database table will be populated and will be used to ensure a unique PIN for each hunt. Eventually the table will look like this:

ScreenshotEggHunt3

Now, you’re ready to tackle push notifications, but setting up push is really tricky for iOS. Remember, you’re going to need to test on a device so you’ll need a paid membership as an Apple Developer.

Push for Android is a little more straightforward, but you’re going to want to read through the documentation carefully to set up your app to receive pushes.

Once you have set up your backend services, I found that adding a little helper utility to display any errors, alerts, or the actual push notifications on the frontend can be quite useful. In the /js folder I added a file called appConsole.js with just one function:

(function ($, undefined) {

    window.appConsole = {
        log: function (message, isError, container) {
            var lastContainer = $(".console div:first", container),
                lastMessage = lastContainer.text();

            if (!lastContainer.length || message !== lastMessage) {
                $("<div" + (isError ? " class='error'" : "") + ">")
                    .css({
                        marginTop: -24,
                        backgroundColor: isError ? "#ffbbbb" : "#72CBFF"
                    })
                    .html(message)
                    .prependTo($(".console", container))
                    .animate({
                        marginTop: 0
                    }, 300)
                    .animate({
                        backgroundColor: isError ? "#ffdddd" : "#ffffff"
                    }, 800);
            } 
        },

        error: function (message) {
            this.log(message, true);
        },

        clear: function () {
            $(".console").html("");
        }
    };
})(jQuery);

This little utility allows any messages to bubble up from the backend and be displayed in a series of unobtrusive frontend alerts. By including this file in my index.html file, I am able to show these messages in a div with class = "console":

if(this.huntPIN.length != 6) {
   appConsole.log("PIN length must be exactly six characters")
   return
 }

Step 2: Build Out Your Host View

We are creating a simple tabbed interface with a header and the navigation taken care of in the bottom tabs. There are only two views, but each view has some nuances.

The Host view has two states – the initial state with an invitation to create a hunt, and a second state where the six-digit code that is created is available to be emailed to users. The HTML is relatively simple.

Create a view with a model and an event triggered on data-show:

<div data-role="view" id="Host" data-title="Host a Hunt" data-model="app.Host.model" data-show="app.Host.events.dataShow">

Create a form to allow data input:

<form>
    <ul data-role="listview" data-style="inset">
       <li class="pin">
           <label>
              Create a PIN:
           <input type="text" name="huntPIN" data-bind="value: huntPIN" required  />
           </label>
      </li>
   </ul>
<button class="btn colored" style="width: 100%;margin:0 auto" data-role="button" data-bind="click: createHunt">Create</button>
</form>

Add a Host.js file to gather your user input and integrate the social sharing plugin. This code snippet allows me to save the input data to the Hunts data type I created earlier:

//el is set earlier as the Everlive Javascript SDK, aka Telerik Backend Services
 var data = el.data('Hunts');
    data.create({ 'huntPIN' : PIN },
        function(data){
          //refresh local storage so we always can show the PIN
          localStorage.setItem("huntPIN",PIN)
          //refresh databinding
          model.set("huntPIN",PIN)
          //show success message
          appConsole.log("Success! Your hunt has been created! Now, invite attendees on the next screen.")
          //send user to the invitation screen
          window.location.href = "#Invite";
        },
       function(error){
         appConsole.log("Oops, there was a problem creating this hunt. Please try again")
});

The host can then invite friends using the huntPIN that is now stored in localStorage:

invite : function(e){
      // check for a configured email client  
          var PIN = localStorage.getItem("huntPIN");
          if(PIN){
             var text = "You've been invited to a hunt! Download the app and input the code "+PIN+""  
               window.plugins.socialsharing.shareViaEmail (
                   text,
                   'You\'ve been invited to a hunt!',
                   null, // TO: must be null or an array
                   null, // CC: must be null or an array
                   null, // BCC: must be null or an array
                   null,
                   this.onSuccess,
                   this.onError
               );  
          }
          else{
              appConsole.log("Please create a hunt PIN before inviting your guests.")
          }

        }

Step 3: Build Out the Attendee Screens

The attendee screens also have two views – the first allows the user to input the PIN and register for push notifications, and the second is to start finding the beacons.

The first thing the attendee sees is a form to input a PIN:

<form>
  <ul data-role="listview" data-style="inset">
    <li class="pin">
       <label>
         My Hunt PIN:
         <input type="text" name="myHuntPIN" data-bind="value: myHuntPIN" required  />
       </label>
    </li>
 </ul>
 <button class="btn colored" style="width: 100%;margin:0 auto" data-role="button" data-bind="click: registerForHunt">Join a Hunt</button>
</form>

This is arguably one of the most interesting pieces of the app – that moment where a user registers for a push segment. By clicking the button above and thereby invoking the registerForHunt function, the attendee kicks off a series of routines for push notifications:

enablePushNotifications : function () {

            var devicePlatform = device.platform; // get the device platform from the Cordova Device API
            appConsole.log("Registering this device for push notifications");

            var currentDevice = el.push.currentDevice(app.constants.EMULATOR_MODE);

            var pushSettings = {
                android: {
                    senderID: 'tbd-when-android-integration-complete'
                },
                iOS: {
                    badge: "true",
                    sound: "true",
                    alert: "true"
                },
                wp8: {
                    channelName: "EverlivePushChannel"
                },
                notificationCallbackWP8: model.onWpPushReceived,
                notificationCallbackAndroid: model.onAndroidPushReceived,
                notificationCallbackIOS: model.onIosPushReceived,
            };

            var customDeviceParameters = {
                "Hunt":localStorage.getItem('registeredHunt')
            };

            currentDevice.enableNotifications(pushSettings)
                .then(
                    function (initResult) {
                        model._onDeviceIsSuccessfullyInitialized();

                        return currentDevice.getRegistration();
                    },
                    function (err) {
                        model._onPushErrorOccurred(err.message);
                    }
                    ).then(
                        function (registration) {
                           model._onDeviceIsAlreadyRegistered();

                            currentDevice
                                .updateRegistration(customDeviceParameters)
                                .then(function () {
                                    model._onDeviceRegistrationUpdated();
                                }, function (err) {
                                    model._onPushErrorOccurred(err.message);
                                });
                        },
                        function (err) {
                            if (err.code === 801) {
                                model._onDeviceIsNotRegistered();

                                currentDevice.register(customDeviceParameters)
                                    .then(function (regData) {
                                        model._onDeviceIsSuccessfullyRegistered();
                                    }, function (err) {
                                        model._onPushErrorOccurred(err.message);
                                    });
                            }
                            else {
                                model._onPushErrorOccurred(err.message);
                            }
                        }
                        );
        },

On the frontend, the user is notified what’s going on:

IMG_6347

There is a lot going on here! We check what the device platform is, make sure we’re not in emulator mode, set up push settings and callbacks, and enable notifications, checking whether the device is already registered (and thus needs to be updated) or whether the device is not yet registered.

Most notable, however, is the customDeviceParameters, a name-value pair that we call “Hunt” and set to the registeredHunt saved in localStorage. This device parameter is going to be sent to the backend via the register routine:

currentDevice.register(customDeviceParameters)

If you’ve set up push properly in the backend with your device registered to receive them, you’ll notice the following:

ScreenshotEggHunt

Your device is now registered to receive push notifications for a hunt segment! How awesome.

Now, let’s close the loop. Once the attendee is all registered for a hunt, the actual searching can begin. Here’s where the Estimote plugin kicks in:

onBeaconsReceived : function(result) {        
    if (result.beacons && result.beacons.length > 0) {
       var msg = "<b>I found " + result.beacons.length + " beacons!";
       for (var i=0; i<result.beacons.length; i++) {
           var beacon = result.beacons[i];
              if(beacon.distance > 0){
                        msg += "<br/>";
                if (beacon.color !== undefined) {
                       msg += "There is a <b>" + beacon.color + "</b> beacon ";
                }                        
           msg += "within " + beacon.distance + " meters of this location.<br/>";
           if (beacon.distance < 1) {
               $("#"+beacon.color+"").show();
                 msg += "When you see it, press the button above!<br/>";
                }
             }
           }
        }
        else {
          var msg = "I haven't found a beacon just yet. Let's keep looking!"
            }
    document.getElementById('beaconlog').innerHTML = msg;
}
});
document.addEventListener('beaconsReceived', model.onBeaconsReceived, false);

We start listening for a signal from a beacon from your bluetooth-enabled device. Then, we start showing some messages to the user, giving the distance and color of the beacon. If the user gets within a meter of the beacon, a button is shown that the user can press. At that point, the beacon is considered to be claimed and the rest of the attendees registered for that hunt are notified:

download

A push is sent to those registered users:

push: function(color) {
            var PIN = localStorage.getItem("registeredHunt");
            var filter = {
              "Parameters.Hunt": PIN
            };
            var notification = {
                "Filter": JSON.stringify(filter),
                "Android": {
                    "data": {
                        "title": "Beacon found!",
                        "message": "The "+color+" beacon was found!",
                        "hunt": PIN
                    }
                },
                "IOS": {
                    "aps": {
                        "alert": "The "+color+" beacon was found!",
                        "badge": 1,
                        "sound": "default",
                        "category": PIN
                    }
                }
            }

            appConsole.log("Notifying members of this hunt...");

            el.push.notifications.create(notification, function (data) {
                appConsole.log("Notification created");
            }, function (err) {
                appConsole.log("Failed to create push notification: " + err.message, true);
            });            
        },

Here, the registeredHunt value that was set earlier in localStorage is sent via a push, matching it to the Parameters.Hunt that we saw earlier associated to a device:

var PIN = localStorage.getItem("registeredHunt");
var filter = {
     "Parameters.Hunt": PIN
};

And, finally, we see the push registered in the backend as having been delivered to the proper segment:

ScreenshotEggHunt4

Here’s a video showing the completed app in action:

What’s next?

While Beacon Hunt seems like a simple app, it has a lot of interesting things going on in the background, including the use of some cool plugins and, of course, push notifications which are always fun to use once you get the setup done!

What’s next for this app? I would like to make this app better able to handle offline situations – you should be able to search for beacons offline as they only require bluetooth, not WiFi or cell phone connectivity, but registering for the push notifications is more challenging if offline. In addition, I’m waiting for my Estimote stickers to arrive and hope to be able to build out the app a little more when they are available.

It would also be nice to not rely exclusively on Estimote beacons – there are many other types on the market. And, of course, I need to release this app for Android. For the time being, however, you can have a pretty epic beacon-enabled Easter egg or Afikoman hunt using this app and Estimote beacons. I wish you a terrific, inspired Spring!

Header image courtesy of Cyndy Sims Parr

Comments