Strutta API Example app for Sinatra
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
definitions
public
views
.gitignore
Gemfile
Gemfile.lock
LICENSE.txt
README.md
app.rb
config_template.rb
cors.rb

README.md

This goal of this tutorial is to give a working example of a Strutta API Sweepstakes in the context of a thick-client application. We will use Sinatra, Angular, and a bit of Twitter Bootstrap, so ideally you have a good working knowledge of Ruby, Javascript, HTML5 and CSS3.

Let's get into it.

In the Strutta API, every type of promotion - Sweepstakes, Contests, etc. - is a Game. Games are made up of Rounds, of which there are several types. For this tutorial, we'll be using Submission, Random Draw, and Webhook Rounds.

Rounds are connected by a Flow - which defines how Entries make their way through a Game. A simple Sweepstakes Flow, as you may have guessed, looks like this:

Submission -> Random Draw -> Webhook

A Contest Flow might look like this:

Submission -> Voting -> Judging -> Webhook

Flows can be much more complex, but for this tutorial, we're going to keep it simple.

If you want to look into the nuts and bolts of how all of this works, you should take a moment to read through the descriptions of each of the entities in the API documentation. There are both JSON and Ruby code examples for each endpoint.

Now that we've covered the intro, let's build a Sweepstakes.

The first thing we'll set up is our local Sinatra server. This is a very simple app with a few endpoints for setting up the Game and for handling the aspects of the API that are too sensitive to be called from the client.

Start by creating a directory for this tutorial.

cd ~/mypath
mkdir strutta-api
cd strutta-api

Next, clone the Sinatra app and setup its dependencies.

git clone https://github.com/struttagit/strutta-api-sinatra-app.git
cd strutta-api-sinatra-app/
bundle install

You'll need a Strutta API account to continue. If you haven't created one yet, visit http://api.strutta.com/.

Before we run the app, we need to do a little bit of configuration. Rename config_template.rb to config.rb, and replace 'mystruttatoken' with your private API key.

Only use your private key on the Server, and make sure you don't check it in to any public repositories.

Once that's done, we can run the app.

ruby app.rb

You can now visit http://localhost:4567/ in your browser and follow the steps for setting up your first Game.

As you step through each, we'll take a look under the hood.

Step 1: Create Game

# [Sinatra app] app.rb:22

# CREATE GAME
post '/create-game' do
  strutta = Strutta::API.new STRUTTA_PRIVATE_TOKEN
  strutta.games.create(GAME_DEFINITION).to_json
end

As you can see, the strutta-api gem does most of the work here. GAME_DEFINITION can be found in definitions/game.rb - you can edit the title and metadata in any way you'd like.

Game creation is the only API action that requires your private token. All other actions can be executed by Participants, assuming they have been granted the required permissions. (See API DOCS - #authentication and API DOCS - #participant-permissions.

Step 2: Record Game ID

This step simply involves copying the Game id from the UI and pasting it into your config.rb. Make sure you restart the server once you're done.

# Restart the server

Ctrl-C
ruby app.rb

Step 3: Create Rounds

# [Sinatra app] app.rb:28

# CREATE ROUNDS
post '/create-rounds' do
  strutta = Strutta::API.new STRUTTA_PRIVATE_TOKEN

  # Create Rounds
  rounds = {
    submission: strutta.games(STRUTTA_GAME_ID).rounds.create(SUBMISSION),
    random_draw: strutta.games(STRUTTA_GAME_ID).rounds.create(RANDOM_DRAW),
    webhook: strutta.games(STRUTTA_GAME_ID).rounds.create(WEBHOOK)
  }

  rounds.to_json
end

Round types and their rules are well defined in the API Documentation - #rounds, so we won't dwell for them too long here. The important thing to note is that way the start and end dates of the Rounds are configured.

ONE_DAY = 60 * 60 * 24
SUBMISSION = {
  ..
  start_date: Time.now.to_i,
  end_date: (Time.now + (ONE_DAY * 7)).to_i,
  ...
}
RANDOM_DRAW = {
  ...
  start_date: (Time.now + (ONE_DAY * 7) + 1).to_i,
  end_date: (Time.now + (ONE_DAY * 8)).to_i,
  ...
}
WEBHOOK = {
  ...
  start_date: (Time.now + (ONE_DAY * 8) + 1).to_i,
  end_date: (Time.now + (ONE_DAY * 9)).to_i,
  ...
}

In more complex Flows, it is possible and often necessary for Rounds to be active concurrently, but for a simple Sweepstakes, avoiding any overlap makes our lives just a little easier. Random Draw and Webhook rounds actually execute their actions when they end, but it's nice to give a little buffer for any support tickets or other unexpected events that may cause the timelines to bend a little.

Step 4: Create Flow

We've now created all of our Rounds, which means we can tie them together. To do that, we first need to collect their ids. This is handled in the browser:

// [Sinatra app] public/setup.js:5

// Intelligible flow data
  var flowData = function() {
    var roundData = JSON.parse(localStorage.rounds);
    return {
      submission_id: roundData.submission.id,
      random_draw_id: roundData.random_draw.id,
      webhook_id : roundData.webhook.id
    };
  };

And converted into a Flow on the server:

# [Sinatra app] app.js:42

# CREATE FLOW
post '/create-flow' do
  content_type :json
  data = JSON.parse request.body.read
  flow_definition = [
    {
      id: data['submission_id'],
      pass_round: data['random_draw_id'],
      start: true
    },
    {
      id: data['random_draw_id'],
      pass_round: data['webhook_id']
    },
    {
      id: data['webhook_id']
    }
  ]
  strutta = Strutta::API.new STRUTTA_PRIVATE_TOKEN
  strutta.games(STRUTTA_GAME_ID).flow.create(definition: flow_definition).to_json
end

See the API Documentation - #flow for an in-depth explanation of the Flow definition.

Moving to the client!

Now that we've got our Game, Rounds and Flow created, we can implement the actual Sweepstakes.

Installation

We've created a bare-bones Angular app that implements the API for this tutorial. The actual Angular implementation is outside the scope of this tutorial, so we'll get the app running and focus on the Strutta integration.

cd ~/mypath/strutta-api
git clone https://github.com/struttagit/strutta-api-tutorial-1
cd strutta-api-tutorial-1/

You need to store your Game id and Public Token as global Javascript variables. Replace the placeholders in app/index.html.

<!-- [Angular app] app/index.html:36 -->

<!-- Add Strutta Game ID and Public Key -->
<script>
  STRUTTA_GAME_ID = GAME_ID;
  STRUTTA_PUBLIC_KEY = 'my_strutta_public_key';

  STRUTTA_API_URI = 'https://api.strutta.com' + STRUTTA_GAME_ID;
</script>

Once you've added your data, install dependencies and run the app.

npm install
npm start

You should now be able to access the Sweepstakes at http://localhost:8000/app.

Integration

Our Sweepstakes has several components that use the Strutta API.

  • Sign up
  • Sign in
  • Sweepstakes entry
  • A second entry given when a Participant shares the Sweepstakes on Facebook

Sign up

For us, a 'sign up' means the creation of a Participant in the context of our Game. Participant creation was designed to be called on the server after a user has authenticated with an existing application. This is to mostly to give contest administrators the opportunity to regulate who can participate in their promotion - any Game can be as inclusive or exclusive as you'd like. We'll be creating the Participant via our Sinatra app to give an example of the workflow from client to server.

Participant objects are made up of an email address and an optional metadata object. For this implementation, the only metadata we'll capture is the Participant's full name.

Let's take a look at our Registration form, which we can reach via the Sign In button in the nav.

<!-- [Angular app] app/registration/registration.html:14 -->

<!-- Registration form -->
  <form ng-submit="registration()" role="form">
    <div class="form-group">
      <label for="name">Full Name</label>
      <input type="textfield" class="form-control" id="name" ng-model="name" placeholder="John Doe" required >
    </div>
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="email" class="form-control" id="email" ng-model="email" placeholder="john.doe@email.com" required >
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
  </form>

If you've used Bootstrap and Angular before, this should look pretty routine. The line to note is the opening of the form tag itself, as well as the tags themselves

<form ng-submit="registration()" role="form">
...
  <input type="textfield" class="form-control" id="name" ng-model="name" placeholder="John Doe" required >
...
  <input type="email" class="form-control" id="email" ng-model="email" placeholder="john.doe@email.com" required >

ng-submit is one of the ways that Angular allows us to call an action of form submission; in this case, the action is $scope.registration(), found our Registration controller. Because of the ng-model attributes on the two input fields, Angular's magic binding gathers our name and email as $scope.name and $scope.email.

Let's take a look at the registration() method:

// [Angular app] app/registration/registration.js:22

// Form submit passed registration request to Strutta API
$scope.registration = function() {

  var redirect = $location.search().redirect || '/';

  // POST to dummy login app (create Strutta Participant)
  $http.post('http://localhost:4567/register', { name: $scope.name, email: $scope.email })
  .success(function(data, status, headers, config) {
    // Store user data in localStorage
    $localStorage.struttaParticipant = data;

    // Redirect on success
    $location.path(redirect);
  })
  .error(function(data, status, headers, config) {
    $scope.regError = true;
    $scope.errorMsg = data.message;
  });
};

We take the data, post it to our Sinatra app, and store the response in localStorage. This allows us to maintain the Participant's 'logged in' state even if they navigate away.

The server-side call is a little more complicated:

# [Sinatra app] app.rb:64

# CREATE STRUTTA PARTICIPANT
post '/register' do
  content_type :json

  # Create participant definition from POST data
  data = JSON.parse request.body.read
  participant_data = {
    email: data['email'],
    metadata: { name: data['name'] }
  }

  begin
    # Create participant using Strutta API
    strutta = Strutta::API.new STRUTTA_PRIVATE_TOKEN
    strutta.games(STRUTTA_GAME_ID).participants.create(participant_data).to_json

  rescue Strutta::Errors::UnprocessableEntityError => message
    status 422
    { message: 'There is already a Participant with this email address, please Sign In instead' }.to_json
  end
end

Once we've parsed our request, the building of the Partipant object is simple, but we do need to catch the error that gets thrown when a user tries to create two accounts with the same email.

Sign In

If a user already has an account, but has either had their token expire or lost their localStorage session, they need the ability to sign in once more. Signing in will renew their token, allowing them to interact with the Sweepstakes once more.

The sign-in form looks very similar to the Registration form, as does the sign in callback.

<!-- [Angular app] app/registration/sign-in.html:8 -->

<!-- Sign In Form -->
  <form ng-submit="signIn()" role="form">
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="email" class="form-control" id="email" ng-model="email" placeholder="john.doe@email.com" required >
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
  </form>
// [Angular app] app/registration/registration.js:45

$scope.signIn = function() {

  var redirect = $location.search().redirect || '/';

  // POST to dummy login app (Renew Participant token)
  $http.post('http://localhost:4567/renew', { email: $scope.email })
  .success(function(data, status, headers, config) {
    // Save updated participant data to localStorage
    $localStorage.struttaParticipant = data;

    // Redirect on success
    $location.path(redirect);
  })
  .error(function(data, status, headers, config) {
    console.log(status);
  });
};

We gather the data and post it to the Sinatra app once more.

# [Sinatra app] app.rb:86

# RENEW PARTICIPANT TOKEN
post '/renew' do
  content_type :json

  # Create participant definition from POST data
  data = JSON.parse request.body.read

  # Ideally you'd have your user's Strutta Participant ID stored in a DB, but if you don't here's how to get it

  # Get participant by email
  strutta = Strutta::API.new STRUTTA_PRIVATE_TOKEN
  participant = strutta.games(STRUTTA_GAME_ID).participants.search(email: data['email'])

  # Renew token if needed
  if participant['token_expired']
    token = strutta.games(STRUTTA_GAME_ID).participants(participant['id']).token_renew(duration: 60 * 60)
    participant['token'] = token['token']
    participant['token_expired'] = token['token_expired']
  end

  participant.to_json
end

In a real-life integration, where Participant creation happens after authentication of some form, it would be ideal to store the Strutta Participant id in association with your own user object. However, if this is impossible or for some reason fails, you can always find a user by email, as we've done in the above code block.

Once the user is found, we check if their token is still active, and renew it if necessary.

Participant tokens are valid for 1 day by default. Ideally their duration matches or exceeds the log-in duration of your application. This way, a new Participant token only needs to be requested with each login to the client.

Sweepstakes Entry

We've build out the Game and all its components, and now we've created Participants to play the game. The next step is to have a Participant create an Entry.

Entries belong to one Participant and one Game, but belong to many different Rounds as they progress through the Game's Flow. A Participant can only enter when a Submission Round is active. The number of entries a user can make is defined in the Submission Round rules.

We will be allowing a user to enter normally, and we will also give them a free entry if they share the Sweepstakes on Facebook. Because of this, the Participant's Entry limit is 2 for the duration of the Game.

We gather our data in app/sweeps/sweeps.html just as we did in the Registration and Sign In forms, so we don't really need to look at that part again.

Let's jump straight to the function called via ng-submit:

// [Angular app] app/sweeps/sweeps.js:15

$scope.sweeps_submit = function() {
  // Create entry for API
  var entry = {
    participant_id: $localStorage.struttaParticipant.id,
    metadata: $scope.entryData,
    token: $localStorage.struttaParticipant.token
  };

  // Save entry data for Double-up entry
  $localStorage.entry = entry

  // Create Entry on the Strutta API
  $http.post(STRUTTA_API_URI + '/entries', entry)
  .success(function(data, status, headers, config) {
    // On success, add trackers for doubleup
    $localStorage.entry.firstId = data.id;
    $localStorage.entry.doubleUp = false;

    // Go to thanks page
    $location.path("/thanks");
  })
  .error(function(data, status, headers, config) {
    console.log(status);
  });
};

This time, we post directly to the Strutta API from the browser.

Note that we added the participant_id to the entry itself. This isn't the prettiest implementation in the world, but since the API expects a the token token either in a Request header or as a URI parameter, it works just fine.

Note that we also store the entry object in localStorage, and update it in the success callback. These simple flags help the UI know how far along our Participant has gotten.

On success, the Participant is redirected to the Thank You page, where they are given the chance to share the Sweepstakes for another entry.

Second Entry on Facebook Share

Facebook's embedded share dialog is incredibly easy to use. You can read their Share Dialog documentation for implementation specifics.

We've already included the Facebook Javascript SDK in index.html, implementing a callback on Share success is simple.

<!-- [Angular app] app/thanks/thanks.html:13 -->

<!-- Facebook Share -->
<a ng-click='fbShare()' href>Share</a>

ng-click simply calls a Javascript function when a user (you guessed it) clicks on the element.

// [Angular app] app/thanks/thanks.js:20

// Documentation: https://developers.facebook.com/docs/sharing/reference/share-dialog
$scope.fbShare = function() {
  FB.ui({
    method: 'share',
    href: 'https://my.sweeps.com',
  }, function(response) {
    // Create a second Entry with the Strutta API
    $http.post(STRUTTA_API_URI + '/entries', $localStorage.entry)
    .success(function(data, status, headers, config) {
      // On success, add trackers for doubleup
      $localStorage.entry.doubleUp = true;
    })
    .error(function(data, status, headers, config) {
      console.log(status);
    });
  });
};

We share the URL of the promotion, and once its complete, we use the Entry data stored in localStorage to POST a second entry. We then record the fact that the user has gotten their double up so that we can modify the UI accordingly.

That's it!

You've now created a Game, defined its Rounds and Flow, authorized Participants to use Enter, and created Entries based on two different user actions. The rest of the action happens behind the scenes of the Strutta API. When the Random Draw Round ends, winners will be picked based on the Random Draw Round's rules. The winners will advance to the Webhook Round, which will post the winner data to the Webhook when the Webhook round ends. It's then up to you to give them their prizes!

I hope that this tutorial provided some insight into the flexibiliy and ease-of-use of the Strutta API. Please let us know if there is anything that is unclear, or any bugs that surface in the demo apps.

Downloaded angular seed: git clone --depth=1 https://github.com/angular/angular-seed.git strutta-api-tutorial-1

Setup dependencies npm install (Calls bower install automatically)

Local storage bower install ngstorage

Run the app with npm start Serves at http://localhost:8000/app/index.html

Bootstrap setup Add the CSS CDN link to index.html from http://getbootstrap.com/getting-started/

AngularStrap handles bootstrap-inspired directives http://mgcrea.github.io/angular-strap/

Sinatra gem install sinatra gem install json

Creating a promo:

The first thing you need to do is create a free account on http://api.strutta.com/. Visit the Account page, and Visit the accounts page and get your private token