Note: This is a public beta feature.
- Use Turbolinks to convert a multi-page app
- Quick start
- Suggested conversion pattern using Turbolinks
- Enabling turbolinks on your app
- Conversion guide
Turbolinks is a JavaScript library that allows your app to behave as if it were a single-page app.
If you have a multi-page server-side rendered (SSR) app and you want to use session token-based authentication, but you are unsure or not yet ready to convert your app to a single-page app, then you can still use session tokens by converting your app to use Turbolinks.
To run this app locally, you can clone this repository and do the following.
- Create a
.env
file to specify yourShopify API Key
andShopify API Secret
available from your partners dashboard.
SHOPIFY_API_KEY='YOUR API KEY FROM SHOPIFY PARTNERS DASHBOARD'
SHOPIFY_API_SECRET_KEY='YOUR API SECRET KEY FROM SHOPIFY PARTNERS DASHBOARD'
SHOPIFY_DOMAIN='YOUR SHOPIFY DOMAIN - DEFAULT myshopify.com'
Note: If you do not have a Shopify API Key or Shopify API Secret, see the following sections of the Build a Shopify App with Node and React guide:
- Run the following to install the required dependencies.
$ bundle install
$ yarn install
$ rails db:migrate
- Ensure ngrok is running on port
3000
.
$ ngrok http 3000
Note: This port number is arbitrary - you may choose to specify the port number you plan to listen to this app on.
- Run the following to start the app.
$ rails s
- Open this sample app in Admin. Requests to authenticated resources, like the
ProductsController
or theWidgetsController
should now be secured with anAuthorization: Bearer <session token>
header.
Above: A sample multi-paged app with an authenticated home page. It displays links to the protected Products and Widgets resources.
Above: Requests made across multiple pages of the app are authenticated using JWTs.
To use session tokens with your multi-page app using Turbolinks, we suggest implementing the pattern below.
- Create an unauthenticated controller that renders a splash page when a user visits your app.
- This page is intended to communicate to your user that your app is loading.
- Use this splash page to:
- Create an App Bridge instance.
- Retrieve and cache a session token within your app client.
- Install event listeners to set an
"Authorization": "Bearer <session token>"
request header on the following events:turbolinks:request-start
turbolinks:render
- Install a timed event that continues to retrieve and cache session tokens every 50 seconds or so.
- This will ensure that your session tokens are always valid.
- Use Turbolinks to navigate to your app's authenticated home page or resource.
Follow the steps below to enable Turbolinks on your app. The official Turbolinks GitHub guide is an excellent resource on getting started.
- Add the
turbolinks
gem to your Gemfile:
gem 'turbolinks', '~> 5'
-
Run
bundle install
. -
Add the
turbolinks
package to your application.
$ yarn add turbolinks
- Add the following line to
app/javascript/packs/application.js
if your app uses webpack to manage its manifest files.
require("turbolinks").start()
This section assumes your app is enabled to use JWT authentication and session tokens. You can create a JWT-enabled app using the v14 shopify_app gem release by running the following generator.
$ rails generate shopify_app --with-session-token
- The
--with-session-token
flag creates an embedded app that is configured to use App Bridge authentication right out of the box.
The sections below describe a step-by-step implementation to the pattern described in Suggested conversion pattern using Turbolinks.
Your splash page is used to indicate that your app has begun to fetch a session token. Once your app has this token, your app should navigate the user to the main view containing potentially protected or authenticated resources.
- Create a
SplashPageController
along with a default index action and view.
$ rails generate controller splash_page index
- Make
splash_page#index
the default root route for your app. Change the following in yourroutes.rb
file.
Rails.application.routes.draw do
root to: 'splash_page#index'
...
end
- Indicate a loading status in your splash page index view. Change
app/views/splash_page/index.html.erb
to match the following.
<p>Loading...</p>
- Make the
SplashPageController
behave as the default embedded appHomeController
.
- Change
app/controllers/splash_page_controller.rb
to match the following.
class SplashPageController < ApplicationController
include ShopifyApp::EmbeddedApp
include ShopifyApp::RequireKnownShop
def index
@shop_origin = current_shopify_domain
end
end
- Protect the default
HomeController
by inheritingAuthenticatedController
. Changehome_controller.rb
to match the following.
class HomeController < AuthenticatedController
def index
end
end
Tip: You can add
include ShopifyApp::RequiredKnownShop
toHomeController
if you would like all requests made to this controller to have a validshop
query parameter.
Note: If your app is an embedded app, one of the first JavaScript files to run on app load is
app/javascript/shopify_app/shopify_app.js
. We can leverage this to fetch and store tokens for the app.
When users visit the app for the first time, they will be presented with the loading splash page. Use this splash page to accomplish the following via JavaScript:
- Create an App Bridge instance
- Fetch a session token and cache it
- Install event listeners on the
turbolinks:request-start
andturbolinks:render
events to add anAuthorization
request header - Install event listeners to add an
Authorization
request header on the following events:turbolinks:request-start
turbolinks:render
- Use Turbolinks to navigate to the
HomeController
- Add the following
load_path
parameter toapp/views/layouts/embedded_app.html.erb
.
...
<%= content_tag(:div, nil, id: 'shopify-app-init', data: {
api_key: ShopifyApp.configuration.api_key,
shop_origin: @shop_origin || (@current_shopify_session.domain if @current_shopify_session),
load_path: params[:return_to] || home_path,
...
} ) %>
...
- This parameter is used by Turbolinks to know where to navigate this app to when a session token has been fetched. In this app, we navigate to
home_path
by default.
- Import the library method
getSessionToken
fromapp-bridge-utils
inapp/javascript/shopify_app/shopify_app.js
.
import { getSessionToken } from "@shopify/app-bridge-utils";
- Write a method in
shopify_app.js
that fetches and stores a session token and another to repeat this every 50 seconds.
async function retrieveToken(app) {
window.sessionToken = await getSessionToken(app);
}
function keepRetrievingToken(app) {
setInterval(() => {
retrieveToken(app);
}, 50000);
}
- In
shopify_app.js
, add event listeners to theturbolinks:request-start
andturbolinks:render
events. Set an"Authorization": "Bearer <session token>"
header during these events.
document.addEventListener("turbolinks:request-start", function (event) {
var xhr = event.data.xhr;
xhr.setRequestHeader("Authorization", "Bearer " + window.sessionToken);
});
document.addEventListener("turbolinks:render", function () {
$("form, a[data-method=delete]").on("ajax:beforeSend", function (event) {
const xhr = event.detail[0];
xhr.setRequestHeader("Authorization", "Bearer " + window.sessionToken);
});
});
- In the same file, edit the
DOMContentLoaded
event listener to add the following instructions.
document.addEventListener("DOMContentLoaded", async () => {
var data = document.getElementById("shopify-app-init").dataset;
var AppBridge = window["app-bridge"];
var createApp = AppBridge.default;
window.app = createApp({
apiKey: data.apiKey,
shopOrigin: data.shopOrigin,
});
var actions = AppBridge.actions;
var TitleBar = actions.TitleBar;
TitleBar.create(app, {
title: data.page,
});
// Wait for a session token before trying to load an authenticated page
await retrieveToken(app);
// Redirect to the requested page
Turbolinks.visit(data.loadPath);
// Keep retrieving a session token periodically
keepRetrievingToken(app);
});
- After a session token is retrieved,
Turbolinks.visit(data.loadPath)
visits theload_path
param defined inembedded_app.html.erb
. - Your app continues to retrieve session tokens every 50 seconds or so.
When a user visits your app, they should now briefly see a "Loading..." screen before they're taken to the HomeController
of your app. This HomeController
is already an authenticated controller, but for this demo we have created two additional authenticated controllers as well: the ProductsController
and the WidgetsController
.
This section shows how the ProductsController
was created and made navigational within the app.
- Generate a
ProductsController
using the Rails generator.
$ rails generate controller products index
- Protect the
ProductsController
by inheritingAuthenticatedController
.
class ProductsController < AuthenticatedController
def index
@products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
end
end
- Create a view for the
ProductsController
. Editapp/views/products/index.html.erb
to match the following.
<%= link_to 'Back', home_path(shop: @shop_origin) %>
<h2>Products</h2>
<ul>
<% @products.each do |product| %>
<li><%= link_to product.title, "https://#{@current_shopify_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
<% end %>
</ul>
Tip: To satisfy a
ShopifyApp::RequireKnownShop
concern, add the following method to thebefore_action
filter withinAuthenticatedController
.shop_origin
ensures your controllers always have a validshop
parameter to add as a query parameter between page navigations.This makes it possible to create "Back" links or navigational breadcrumbs to resources that include the
ShopifyApp::RequiredKnownShop
concern.
class AuthenticatedController < ApplicationController
include ShopifyApp::Authenticated
before_action :shop_origin
def shop_origin
@shop_origin = current_shopify_domain
end
end
current_shopify_domain
is available through theLoginProtection
concern.
- Edit
app/views/home/index.html.erb
to create a link from theHomeController
to theProductsController
view.
<%= link_to 'Products', products_path %>
- Your app is now able to access the authenticated
ProductsController
from theHomeController
using session tokens.