Simple Elm app with Google authentication via ports

2018-06-15

Here’s my first real use of ports in an Elm application. It’ll be light on the commentary, heavy on code.

Overview

This example will use the Google API client for JavaScript to perform OAuth2 authentication using a user’s Google credentials. This will integrate into an Elm application using ports. Specifically:

There’ll be a small amount of JavaScript glue code in index.js which will take care of marshalling the ports to and from JavaScript functions.

On the Elm side of things, the application will provide the bare minimum components to display the results of these calls via the ports mechanism to the user.

Note that I use the following packages/tools in this project:

The application can be run by typing make at the command line:

.gitignore

You’ll need to create your google-api-client-id.txt file by grabbing your Google API client ID after consulting the following resources:

/elm-stuff/
/elm.js
/google-api-client-id.txt
/jquery.min.js
view raw .gitignore hosted with ❤ by GitHub

elm-package.json

This file is typically updated using the elm-package command.

{
"version": "1.0.0",
"summary": "Simple Elm app with Google authentication via ports",
"repository": "https://github.com/rcook/not-a-real-repo.git",
"license": "MIT",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/http": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

index.html

<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="/jquery.min.js"></script>
<script src="/elm.js"></script>
<script src="/index.js"></script>
</head>
<body></body>
</html>
view raw index.html hosted with ❤ by GitHub

index.js

This is the main JavaScript application code:

"use strict";
const Util = (() => {
const me = {};
function formatNativeError(e) {
if (e.hasOwnProperty("message") && e.hasOwnProperty("stack")) {
return {
message: e.message,
stack: e.stack
};
}
return e;
}
me.stubPorts = app => {
const stubs = {};
for (const k in app.ports) {
if (app.ports[k].subscribe) {
stubs[k] = () => { throw `No handler subscribed to Elm port "${k}"`; };
app.ports[k].subscribe(stubs[k]);
}
}
return stubs;
};
me.loadScript = url => Promise.resolve($.getScript(url));
me.loadGoogleApi = libs => new Promise(resolve => gapi.load(libs, resolve));
me.sendNativeError = (app, e) => app.ports.nativeError.send(formatNativeError(e));
// Sometimes useful for simulating slow network etc.
//me.delay = (ms) => new Promise(resolve => window.setTimeout(resolve, ms));
return me;
})();
function initAuth(stubs, app, auth) {
function sendAuthStateChanged() {
if (auth.isSignedIn.get()) {
const currentUser = auth.currentUser.get();
const userId = currentUser.getId();
const basicProfile = currentUser.getBasicProfile();
const fullName = basicProfile.getName();
const email = basicProfile.getEmail();
const authResponse = currentUser.getAuthResponse();
const accessToken = authResponse.id_token;
app.ports.authStateChanged.send({
userId: userId,
fullName: fullName,
email: email,
accessToken: accessToken
});
}
else {
app.ports.authStateChanged.send({});
}
}
const handlers = {
signIn: () => {
if (auth.isSignedIn.get()) {
sendAuthStateChanged(app);
}
else {
auth.signIn({ prompt: "login" });
}
},
signOut: () => {
if (auth.isSignedIn.get()) {
auth.signOut();
}
else {
sendAuthStateChanged();
}
}
};
for (const k in stubs) {
const handler = handlers[k];
if (handler) {
app.ports[k].unsubscribe(stubs[k]);
app.ports[k].subscribe((() => handler()));
}
}
auth.isSignedIn.listen(sendAuthStateChanged);
sendAuthStateChanged();
}
$(() => {
const app = Elm.Main.fullscreen();
const stubs = Util.stubPorts(app);
Util.loadScript("https://apis.google.com/js/platform.js")
.then(() => Util.loadGoogleApi("auth2"))
.then(() => $.get("/google-api-client-id.txt"))
.then(responseText => responseText.trim())
.then(clientId => gapi.auth2.init({ clientId: clientId, scope: "profile" }))
.then(() => initAuth(stubs, app, gapi.auth2.getAuthInstance()))
.catch(e => Util.sendNativeError(app, e));
});
view raw index.js hosted with ❤ by GitHub

Interop.elm

port module Interop
exposing
( authStateChanged
, nativeError
, signIn
, signOut
)
import Json.Decode exposing (Value)
port nativeError : (Value -> msg) -> Sub msg
port authStateChanged : (Value -> msg) -> Sub msg
port signIn : () -> Cmd msg
port signOut : () -> Cmd msg
view raw Interop.elm hosted with ❤ by GitHub

Main.elm

module Main exposing (main)
import Html exposing (program)
import Model exposing (Model, init)
import Msg exposing (Msg)
import Update exposing (update)
import Subscriptions exposing (subscriptions)
import View exposing (view)
main : Program Never Model Msg
main =
program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
view raw Main.elm hosted with ❤ by GitHub

Makefile

.PHONY: live
live: jquery.min.js
elm-live Main.elm --output=elm.js --open
jquery.min.js:
wget https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
view raw Makefile hosted with ❤ by GitHub

Model.elm

module Model
exposing
( Model
, AuthState(..)
, UserInfo
, init
)
type alias Model =
{ authState : AuthState
, message : Maybe String
}
type AuthState
= Unknown
| SignedIn UserInfo
| SignedOut
type alias UserInfo =
{ userId : String
, fullName : String
, email : String
, accessToken : String
}
init : ( Model, Cmd msg )
init =
( { authState = Unknown, message = Nothing }, Cmd.none )
view raw Model.elm hosted with ❤ by GitHub

Msg.elm

module Msg exposing (Msg(..))
import Json.Decode exposing (Value)
import Model exposing (AuthState)
type Msg
= NativeError Value
| AuthStateChanged (Result String AuthState)
| SignInClicked
| SignOutClicked
view raw Msg.elm hosted with ❤ by GitHub

Subscriptions.elm

module Subscriptions exposing (subscriptions)
import Interop exposing (authStateChanged, nativeError)
import Json.Decode exposing (Decoder, oneOf, string, succeed)
import Json.Decode as Decode
import Json.Decode.Pipeline exposing (decode, optional, required)
import Model exposing (AuthState(..), Model, UserInfo)
import Msg exposing (Msg(..))
decodeSignedIn : Decoder AuthState
decodeSignedIn =
decode UserInfo
|> required "userId" string
|> required "fullName" string
|> required "email" string
|> required "accessToken" string
|> Decode.map SignedIn
decodeSignedOut : Decoder AuthState
decodeSignedOut =
succeed SignedOut
decodeAuthState : Decoder AuthState
decodeAuthState =
oneOf
[ decodeSignedIn
, decodeSignedOut
]
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
[ authStateChanged (AuthStateChanged << Decode.decodeValue decodeAuthState)
, nativeError NativeError
]

Update.elm

module Update exposing (update)
import Interop exposing (signIn, signOut)
import Model exposing (Model, AuthState(..))
import Msg exposing (Msg(..))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NativeError value ->
( { model | message = Just <| toString value }, Cmd.none )
AuthStateChanged result ->
case result of
Ok authState ->
( { model | authState = authState }, Cmd.none )
Err m ->
( { model | authState = Unknown, message = Just m }, Cmd.none )
SignInClicked ->
( model, signIn () )
SignOutClicked ->
( model, signOut () )
view raw Update.elm hosted with ❤ by GitHub

View.elm

module View exposing (view)
import Html exposing (Html, br, button, div, li, text, ul)
import Html.Attributes exposing (disabled)
import Html.Events exposing (onClick)
import Model exposing (Model, AuthState(..))
import Msg exposing (Msg(..))
view : Model -> Html Msg
view model =
div []
[ text <| toString model.authState
, button [ disabled <| not <| isSignedOut model.authState, onClick SignInClicked ] [ text "Sign in" ]
, button [ disabled <| not <| isSignedIn model.authState, onClick SignOutClicked ] [ text "Sign out" ]
, br [] []
, case model.message of
Nothing ->
text "No message"
Just m ->
text m
, br [] []
, case model.authState of
SignedIn state ->
ul []
[ li [] [ text state.userId ]
, li [] [ text state.fullName ]
, li [] [ text state.email ]
, li [] [ text state.accessToken ]
]
_ ->
text "No user information"
]
isSignedIn : AuthState -> Bool
isSignedIn state =
case state of
SignedIn _ ->
True
_ ->
False
isSignedOut : AuthState -> Bool
isSignedOut state =
case state of
SignedOut ->
True
_ ->
False
view raw View.elm hosted with ❤ by GitHub

Repository

Check out the full project here.

Tags

Elm
Google
JavaScript
Promise
jQuery
Ports
Interoperability

Content © 2025 Richard Cook. All rights reserved.