🇺🇸 English | 🇪🇸 Español

MapKit JS maps in Ruby on Rails and Stimulus

Since I’m already paying for Apple’s developer account, I wanted to make use of their map service to develop Pizza Pals.

Apple provides all the necessary documentation to use MapKit JS. In this post, I’m going to explain how I integrated MapKit into a Rails app.

We need to create a controller with it’s route and it’s view:

$ bin/rails g controller maps index

The documentation offers the script that we should insert to load MapKit. You can place it in the view you just generated or in the application’s header.

<script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js"
    crossorigin async
    data-callback="initMapKit"
    data-libraries="services,full-map,geojson"></script>

The next thing to do is generate a Stimulus controller. We don’t really need to do this 🤷‍♂️ we could just use the provided Javascript. But I decided to complicate our lives a little bit.

$ bin/rails g stimulus maps

Update the view adding the div that will contain the map. Specify it’s width and height, or you won’t be able to see the map. Give it an id and, since se decided to use Stimulus, call the controller.

# app/views/maps/index.html.erb

<div style="width: 600px; height: 300px" id="map-container" data-controller="maps"></div>

Apple provides examples of how to use the library. I’m going to base it on the Embedded Map example. You may need a developer account to access this link.

Using Stimulus, the code will look something like this:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="maps"
export default class extends Controller {
  async connect() {
    this.initMap()
  }

  async setupMapKit() {
    if (!window.mapkit || window.mapkit.loadedLibraries.length === 0) {
      await new Promise(resolve => { window.initMapKit = resolve });

      delete window.initMapKit;
    }

    const jwt = "Insert Your JWT Here";
    mapkit.init({
        authorizationCallback: done => { done(jwt); }
    });
  }

  async initMap() {
    await this.setupMapKit()

    const cupertino = new mapkit.CoordinateRegion(
      new mapkit.Coordinate(37.3316850890998, -122.030067374026),
      new mapkit.CoordinateSpan(0.167647972, 0.354985255)
    );

    const map = new mapkit.Map("map-container");
    map.region = cupertino;
  }
}

Since we haven’t generated a valid jwt yet, we will see an error when visiting the page. [MapKit] Authorization token is invalid.

Let’s fix it. We need a map identifier and a private key. Once created, go to the dashboard and select create token. We’re going to need the data we just generated on the previous step. You can leave domain restriction empty or set it to localhost.

Dashboard

Copy the token in the code replacing the string: const jwt = "Insert Your JWT Here";

If you start the server and go to the route you’ve created, you should see the map. If you don’t see anything, make sure you’ve provided width and height to the div.

Managing tokens this way is cumbersome and risky. Therefore, we’re going to create an endpoint in our backend that generates tokens every time a map is loaded. You can specify the expiration of a token and reuse it as many times as you want within a year.

Replace in the Stimulus controller:

const jwt = "Insert Your JWT Here";
mapkit.init({
    authorizationCallback: done => { done(jwt); }
});
mapkit.init({
    authorizationCallback: done => {
      fetch("/tokens/mapkit")
        .then(res => res.text())
        .then(done)
    }
})

Using fetch, the map initializer will make an HTTP request to a route on our server that will return the token. This way, we don’t have it hardcoded.

Create another controller. In this case, I’ve named it tokens with a mapkit method. Also, add a route. We don’t need a view for this.

$ bin/rails g controller tokens mapkit

Inside of the controller, in the mapkit method, you can return the token:

def mapkit
  render plain: "[replace with your token]"
end

We’re not there yet, but we’re getting close. We went from having the token hardcoded in the frontend to have it hardcoded in the backend. Now, instead of returning always the same token, let’s generate a new one each request.

Let’s create a Mapkit class with a token static method. Update the controller to use it:

def mapkit
  render plain: Mapkit.token
end

Now, create a new file wherever you feel appropriate inside of your Rails app.

You can return our token from this new class:

class Mapkit
  class << self
    def token
      "[replace with your token]"
    end
  end
end

We’re going to need some help with the jwt. I’ve decided to use the jwt gem but you can use whatever you want.

$ bundle add jwt

The documentation states that to construct the token, we need two components: a header and a payload. Each of them consists of different elements:

In the header, we need to specify the algorithm, which must be ES256. We also need to specify the key id we obtained earlier, and finally, indicate that the typ is “JWT”.

In the payload, we need to specify the team id we obtained earlier, the token generation date (which is now), the token expiration date (up to one year), and finally, the origin. Since we are developing locally, I’ve set it to localhost. In production and other environments, you will need to specify the domain from which it will be used; otherwise, the token will be invalid, and the maps won’t display.

class Mapkit
  class << self
    ALGORITHM = "ES256"

    def token
      header = {
        alg: ALGORITHM,
        kid: "[replace with your key id]",
        typ: "JWT"
      }

      payload = {
        iss: "[replace with your team id]",
        iat: Time.now.to_i,
        exp: 1.day.from_now.to_i,
        origin: "localhost"
      }

      JWT.encode(payload, private_key, ALGORITHM, header)
    end

    private

      def private_key
        OpenSSL::PKey::EC.new("-----BEGIN PRIVATE KEY-----
RANDOM STRING INSIDE THE .P8 FILE
-----END PRIVATE KEY-----")
      end
  end
end

If you look at the private_key method, you’ll notice that the content isn’t indented. This caused me some trouble. You need to include the content exactly as it appears in the .p8 file you downloaded; otherwise, you’ll encounter errors like OpenSSL::PKey::ECError (invalid curve name):.

If you restart the server, everything should continue to work correctly. We’re almost finished.

Obviously, since this is sensitive information, we won’t leave it exposed.

You can store the keys in environment variables. I’ve chosen to do this in Rails credentials:

$ EDITOR="code --wait" bin/rails credentials:edit

You won’t need the EDITOR variable if you don’t use VS Code.

mapkit:
  key_id: your-key-id
  team_id: your-team-id
  key_file: |
    -----BEGIN PRIVATE KEY-----
    random text
    -----END PRIVATE KEY-----

Pay close attention to the | symbol after key_files. It’s part of YAML and is crucial as it preserves line breaks. If you omit it or use the > symbol as sometimes done, you’ll encounter the same OpenSSL::PKey::ECError (invalid curve name): error as before. If you believe you’re doing it correctly and still encounter the error, try restarting the server. It’s possible that the credentials haven’t been reloaded correctly.

Latest posts

Projects

Get updates on my projects (in Spanish 🇪🇸)

    Name
    Email

    Where to find me