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.
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.