Shopify app with Rust: Part 6 – request shop data

The previous post (get OAuth access token) covered retrieving the OAuth access token that can be used for persistent permission to retrieve a shop’s data. To make use of that in the context of an application, we’ll need to attach it to a request. There are a lot of small supporting pieces here, so you’ll need a bit of patience.

First things first, if you’re actually coding along then when you open the application within Shopify’s dashboard, you’re likely seeing something disappointingly like:

Firefox can’t open this page

This is a security feature that is enabled by Rocket by default that prevents our application from being embedded within another webpage (e.g. the Shopify partner dashboard). Specifically, this is the Rocket shield. There are still some pieces of Rocket that are unimplemented, e.g. support for Content-Security-Policy, so let’s disable some protection entirely to make it embeddable!

// main.rs

use rocket::{
    ...
    shield::{Frame, Shield},
    ...
};
...

fn rocket() -> rocket::Rocket<rocket::Build> {
    rocket::build()
        .mount("/", routes![install, callback, index])
        .attach(Shield::default().disable::<Frame>())
}
...

With the X-Frame-Options header removed (via Frame), the page should now be embeddable within the Shopify portal (not that we have much to show…).

Making a really good and not at all terrible UI

Aligned with the theme of this series, lets just barrel forwards naively and fix stuff when it doesn’t work. Any self respecting application needs some form of UI, so far we’re lacking that regard. Rather than add that logic in our /install or /callback request handlers, lets add a new API. As these blog posts are not about UI technologies, we’re going to do about the laziest possible thing and return static HTML directly with no supporting tooling. We’ll spice it up just a tiny bit by making it parameterizable with the shop’s name:

// main.rs

fn build_index_html(shop: &str) -> String {
	format!(
		r#"<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>R'al cool Shopify app</title>
  <script>
    function doFancyFrontEndThings() {{
      window.alert("Ya dun clicked the button");
    }}
  </script>
</head>
<body>
  <h1>Welcome to this Shopify Application, {}</h1>
  <button onclick="doFancyFrontEndThings()">Click me!</button>
</body>
</html>"#,
		shop,
	)
}

Add a new route that returns this page so that we can see our really rockin’ UI:

// main.rs
use rocket::{
    response::{content, Redirect},
    ...

#[rocket::get("/")]
pub async fn index() -> Result<content::Html<String>, &'static str> {
    Ok(content::Html(build_index_html("user"))
}
...

fn rocket() -> rocket::Rocket<rocket::Build> {
    rocket::build()
        .mount("/", routes![install, callback, index])
        ...

We also need a mechanism to see this page, so how about we retool our /callback to redirect here:

// main.rs
use rocket::{
    ...
    uri, State,
};
...

#[rocket::get("/callback?<code>&<shop>")]
pub async fn callback(
    code: &str,
    shop: &str,
    installations: &State<Installations>,
) -> Result<Redirect, &'static str> {
    ...
    Ok(Redirect::to(uri!(index())))
}

Now if we visit the app again we should actually be able to see HTML, which is dramatically cooler than the terribleness we had previously:

Glorious rendered HTML!

Feels like we’re getting close… an installable application with a UI?! The big piece that’s missing is getting Shopify data.

Authorized request to Shopify

Currently we have the access token for our test shop being printed out into the application server logs (bad practice!), so lets use it. Run the application and save the access token somewhere, we’ll hardcode it shortly. Shopify has a number of different APIs and a mix off REST based and GraphQL based interfaces. For some extra complexity, there’s not feature parity between the two. In general, it seems the GraphQL version is intended to be the “future”, so we’ll go down that route. Most APIs relevant for simple apps will come from the Admin API section.

If you’re trying to craft your own query, the Shopify GraphiQL explorer is a great tool to use. If you were doing this for real, you’d likely use a GraphQL client in Rust (e.g. graphql_client), but to keep our dependencies as minimal as possible we’re going to cut some corners. We will add the serde_json crate though, as a halfway between unstructured text and full deserialized structs.

// Cargo.toml
...
[dependencies]
rocket = "0.5.0-rc.1"
reqwest = { version = "0.11", features = ["json"] }
serde = "1"
serde_json = "1"

As we’re not trying to tackle “how to work with Shopify’s API in general”, lets use retrieving the shop’s name as placeholder logic. To do so, we’ll need to form a GraqhQL request and submit it to the right endpoint. Well need 4 pieces to build this HTTP POST:

  1. the endpoint: https://{shop}.myshopify.com/admin/api/2021-07/graphql.json
  2. the Content-Type header: Content-Type: application/graphql
  3. the shop’s access token as a header: X-Shopify-Access-Token: {token}
  4. The actual GraphQL query

As with the HTML, make a constant for the very simple GraphQL query. In a real app instead of getting the shop’s name you might get recent orders or customers:

// main.rs

const QUERY: &str = r#"
{
  shop {
    name
  }
}"#;

Then build and submit the request. Note that the access token is a password and as such should not be shared. This has red flags all over it.

//main.rs
use reqwest::header::CONTENT_TYPE;
...
const APPLICATION_GRAPHQL: &str = "application/graphql";
const SHOPIFY_ACCESS_TOKEN: &str = "X-Shopify-Access-Token";

#[rocket::get("/")]
pub async fn index() -> Result<content::Html<&'static str>, &'static str> {
	let admin_api_uri = format!(
		"https://{}.myshopify.com/admin/api/2021-07/graphql.json",
		"toby-test-store2"
	);

	let client = reqwest::Client::new();
	let response = client
		.post(admin_api_uri)
		.header(CONTENT_TYPE, APPLICATION_GRAPHQL)
		.header(
			SHOPIFY_ACCESS_TOKEN,
			"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",
		)
		.body(QUERY)
		.send()
		.await
		.map_err(|_| "Error sending request")?;

	let response_body: serde_json::Value = response
		.json()
		.await
		.map_err(|_| "Error getting response body as json")?;

	let pretty_json = serde_json::to_string_pretty(&response_body).map_err(|_| "Error prettifying JSON")?;

	println!("Response is: {:#?}", pretty_json);
	Ok(content::Html(HTML))
}
...

When I run this, I get a response that looks something like:

{
  "data": {
    "shop": {
      "name": "Toby Test Store2"
    }
  },
  "extensions": {
    "cost": {
      "actualQueryCost": 1,
      "requestedQueryCost": 1,
      "throttleStatus": {
        "currentlyAvailable": 999,
        "maximumAvailable": 1000.0,
        "restoreRate": 50.0
      }
    }
  }
}

If we traverse this JSON object with wild abandon, we can update our UI so it’s outrageously personalized:

// main.rs
...
	let response_body: serde_json::Value = response
		.json()
		.await
		.map_err(|_| "Error getting response body as json")?;

	let shop_name = &response_body["data"]["shop"]["name"].as_str().unwrap();

	Ok(content::Html(build_index_html(shop_name)))
}
...
Whoa – data from the API in our UI!

Status

Where we started in this post: We had an installable application that would solicit an OAuth access token, but not do anything with it

Where we ended with this post: We tied up a couple loose ends, built a dynamite UI, made a request with the OAuth access token, and served up a UI with data retrieved from Shopify. Things are looking particularly legit around these parts.

Next post: We’ll avoid hard coding the very secret OAuth access token in the browser and store it in memory in Rocket instead

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s