Showing posts with label shopify. Show all posts
Showing posts with label shopify. Show all posts

Sunday, December 1, 2024

Custom Font Shopify Checkout Example

Now that you got your Shopify website up and running. Why not add some branding on your checkout page? In this example, we'll add a custom font to our checkout page. This blog assumes you have read my past blogs about Shopify Checkout Extensibility and Shopify in general. I will not write about how to set up a development store and a checkout app. Please read my past blogs on how to do it. I will just go straight into how to configure the custom font for your checkout app.

Default Font

Below is the default font for the checkout page. This is what it looks like before branding our checkout page.

Upload the WOFF File

Upload the custom font (e.g. Playmaker_D.woff) to Shopify Admin > Content > Files. Custom fonts must be in either Web Open Font Format (WOFF) or WOFF2. Once uploaded, we'll need the gid of the WOFF file. There are two ways of grabbing the gid. One is by inspecting it in web tools and it's usually the ID of the TR element. Second is by sending a GraphQL query. We use GraphiQL to run our queries. Make sure the GraphiQL app has the read_checkout_branding_settings and write_checkout_branding_settings access scopes enabled. As you will see below, they both show the same gid.

Here's the GraphQL query for it.

  
query queryFiles {
  files(first: 10, query: "media_type:GenericFile") {
    edges {
      node {
        ... on GenericFile {
          id
          url
          fileStatus
        }
      }
    }
  }
}
  

Checkout Profile ID

Next, we'll need the checkout profile ID. This is the target for the custom font changes. Go to Checkout > Customize. The profile ID should appear on the URL address bar after /profiles as can be seen below.

Apply Custom Font

Apply the custom font to primary and secondary surfaces. Primary surfaces include text and buttons. Secondary surfaces include headings. You should see the changes right away. And that is all there is to it. Super simple, isn't it?

GraphQL query:

  
mutation checkoutBrandingUpsert($checkoutBrandingInput: CheckoutBrandingInput!, $checkoutProfileId: ID!) {
  checkoutBrandingUpsert(checkoutBrandingInput: $checkoutBrandingInput, checkoutProfileId: $checkoutProfileId) {
    checkoutBranding {
      designSystem {
        typography {
          primary {
            base {
              sources
              weight
            }
            bold {
              sources
              weight
            }
            name
          }
          secondary {
            base {
              sources
              weight
            }
            bold {
              sources
              weight
            }
            name
          }
        }
      }
    }
    userErrors {
      code
      field
      message
    }
  }
}
  

GraphQL variables:

  
{
  "checkoutProfileId": "gid://shopify/CheckoutProfile/",
  "checkoutBrandingInput": {
    "designSystem": {
      "typography": {
        "primary": {
          "customFontGroup": {
            "base": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 100
            },
            "bold": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 500
            }
          }
        },
        "secondary": {
          "customFontGroup": {
            "base": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 100
            },
            "bold": {
              "genericFileId": "gid://shopify/GenericFile/",
              "weight": 500
            }
          }
        }
      }
    }
  }
}
  

As can be seen below, the custom font has been applied.

More Information

For more information, jump to the ultimate source of this process, Shopify Customize typography. Happy Shopify Checkout Extensibilty branding.

Monday, August 19, 2024

Shopify Function Extension Example

This blog builds upon my previous blog, Shopify Check UI Extension Example. Now that we got our Checkout Extensibility app running, it's time to add another payment method. Why do we need to add another payment method? Surely, that's commen sense is the answer to that, right? Having more payment options will let your store cater to more potential customers.

One popular payment method is Paypal. To activate Paypal, on the Shopify admin page, we go the Settings > Payments > Activate Paypal and then follow the instructions. The payment setup would look like below.

Fantastic. Now that we have activated Paypal, we should see something like below on checkout.

Paypal Express Checkout Problem

Well and good that we can accept Paypal payments. But the issue now is that some customers are abandoning the checkout step because they don't have Paypal and it is right at the top of the page. It is making them believe that it is the only payment option. It is confusing the customers especially on mobile view because it's the first thing they see as it is positioned at the top. Our simple requirement now is keep the Paypal payment method and at the same time remove or hide the Paypal express checkout button. Sounds easy enough?

Shopify Function Solution

As mentioned earlier, we are building the Shopify Function extension from my previous blog, Shopify Check UI Extension Example. I'm assuming you have the GitHub repository for it. If not, you can pull from github.com/jpllosa/checkout-practice-app. First off the bat is to generate the extension like so. I've named the extension hide-paypal-express and coding it in JavaScript.

  
C:\shopify\checkout-practice-app>npm run generate extension

> checkout-practice-app@1.0.0 generate
> shopify app generate extension


To run this command, log in to Shopify.
👉 Press any key to open the login page on your browser
✔ Logged in.
╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Type of extension?
√  Payment customization - Function

?  Name your extension:
√  hide-paypal-express

?  What would you like to work in?
√  JavaScript


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Your extension was created in extensions/hide-paypal-express.                 │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

  

Update Scopes

Our scopes on the shopify.app.toml should have been updated with the read_payment_customizations and write_payment_customizations. If not, add them like below and do a deploy to push the scopes to the Partner dashboard.

  
# shopify.app.toml
...snipped...
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_payment_customizations,write_payment_customizations"
...snipped...
  
  
C:\shopify\checkout-practice-app>npm run deploy

> checkout-practice-app@1.0.0 deploy
> shopify app deploy

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│    • Include config:  Yes                                                      │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Release a new version of checkout-practice-app?
√  Yes, release this new version


Releasing a new app version as part of checkout-practice-app

free-shirt-1000     │ Bundling UI extension free-shirt-1000...
hide-paypal-express │ Building function hide-paypal-express...
hide-paypal-express │ Building GraphQL types...
free-shirt-1000     │ free-shirt-1000 successfully built
hide-paypal-express │ Bundling JS function...
hide-paypal-express │ Running javy...
hide-paypal-express │ Done!


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  New version released to users.                                                │
│                                                                                │
│  checkout-practice-app-2 [1]                                                   │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://partners.shopify.com/31716511/apps/1338089799691/versions/3168605962251
  

Enable the Shopify Function

Next step is to grab the function ID to enable the Shopify function and create the payment customization. For this I've installed GraphiQL app to make executing queries and mutations against my store easier. You can find the function ID the Partners page like below.

Run the paymentCustomizationCreate mutation via the GraphiQL app to enable your Shopify function. Don't forget to replace the function ID. You should have a response like below after that.

  
mutation {
  paymentCustomizationCreate(paymentCustomization: {
    functionId: "<replace with function ID>"
    title: "checkout practice app"
    enabled: true
  }) {
    paymentCustomization {
      id
    }
    userErrors {
      message
    }
  }
}
  

Response

  
{
  "data": {
    "paymentCustomizationCreate": {
      "paymentCustomization": {
        "id": "gid://shopify/PaymentCustomization/283772501"
      },
      "userErrors": []
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 10,
      "actualQueryCost": 10,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1990,
        "restoreRate": 100
      }
    }
  }
}
  

Just provide your shop domain to install the GraphiQL App.

To check what payment customizations have been enabled on your store, run the paymentCustomizations query like below.

  
query {
  paymentCustomizations(first: 100) {
    edges {
      node {
        id
        title
      }
    }
  }
}
  

Response

  
{
  "data": {
    "paymentCustomizations": {
      "edges": [
        {
          "node": {
            "id": "gid://shopify/PaymentCustomization/283772501",
            "title": "checkout practice app"
          }
        }
      ]
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 11,
      "actualQueryCost": 3,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1997,
        "restoreRate": 100
      }
    }
  }
}
  

Hide Paypal Express Checkout

Before we hide the Paypal Express Checkout button, let's see how it looks without any logic added to the scaffolding code. Run npm run dev. Go to the checkout page, the top of the page will show the Paypal Express checkout and the bottom will have the standard Paypal payment method. Next, go to your Partners dashboard then your app extensions runs. You should see the logs on what goes in, out and errors. It's empty for now because we haven't added a query and applied any logic to the result of the query.

Under extensions/hide-paypal-express, edit the run.graphql with the query below. This will fetch a list of payment methods available on our checkout.

  
query RunInput {
  paymentMethods {
    id
    name
  }
}
  

On the same directory, edit run.js as below. What this code does is fairly simple. Probably does not need any explanation as it is fairly readable and understandable. We just check for a Paypal Express Checkout payment menthod and if it exists apply the hide operation to it by providing the payment method ID and placement. Express checkout is AcceleratedCheckout. Otherwise, return with no changes to the payment methods.

  
// @ts-check

import { PaymentCustomizationPaymentMethodPlacement } from "../generated/api";

/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

/**
 * @type {FunctionRunResult}
 */
const NO_CHANGES = {
  operations: [],
};

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const hidePaymentMethod = input.paymentMethods.find(
    (method) =>
      method.name.toLocaleLowerCase().trim() === "paypal express checkout"
  )

  if (hidePaymentMethod) {
    return {
        operations: [
        {
          hide: {
            paymentMethodId: hidePaymentMethod.id,
            placements: [PaymentCustomizationPaymentMethodPlacement.AcceleratedCheckout]
          }
        }
      ]
    }
  }

  return NO_CHANGES;
};
  

Save your changes and the changes should hot reload. If not, do npm run dev again. Go to the checkout page and the Paypal Express Checkout button should be gone now.

Debugging a Shopify Function

To debug a shopify function, go to your Partners dashboard then your app extensions runs as described above. This time you should see some logs because we have added a query in run.graphql and we have returned an operation. You should have something like below. If you want to dump some values of variables for example, calls to console.error are shown under Logs (STDERR).

Input (STDIN)

  
{
  "paymentMethods": [
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/0",
      "name": "(for testing) Bogus Gateway"
    },
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/1",
      "name": "Deferred"
    },
    {
      "id": "gid://shopify/PaymentCustomizationPaymentMethod/2",
      "name": "PayPal Express Checkout"
    }
  ]
}
  

Output (STDOUT)

  
{
  "operations": [
    {
      "hide": {
        "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2",
        "placements": [
          "ACCELERATED_CHECKOUT"
        ]
      }
    }
  ]
}
  

Shopify Function Closing

There you have it. A handy way to hide the Paypal Express Checkout option in Shopify Checkout. As usual, entire code is available at github.com/jpllosa/checkout-practice-app. Thank you for reading.

Sunday, July 7, 2024

Shopify Checkout UI Extension Example

As you might have already read on the Shopify website, checkout.liquid is deprecated and stores need to upgrade to Checkout Extensibility by August 13, 2024. So here is an example of how to build a Checkout UI extension.

For this example, our requirement is going to be to provide a free item (e.g. t-shirt) on checkout when the total purchase is greater than a thousand. Clear enough?

Checkout UI Extension Setup

If you have read my past blogs then you probably already have a development store and partner account. In any case, these are the prerequisites:

  • A Shopify Partner account
  • A development store that use the Checkout and Customer Accounts Extensibiliy
  • Shopify CLI

This example was created on a Windows 11 machine with Node v20.11.0, npm v10.2.4 and Shopify CLI v3.60.1. For the final check, on the bottom left of your Shopify Admin page, it should say Checkout and Customer Accounts Extensibility as shown below.

Checkout UI Extension Scaffolding

Super simple, run shopify app init to create you project. You can choose what name you like for your project. In this example, our project name is checkout-practice-app

  
C:\shopify>shopify app init

Welcome. Let’s get started by naming your app project. You can change it later.

?  Your project name?
√  checkout-practice-app

?  Get started building your app:
√  Start by adding your first extension

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Initializing project with `npm`                                               │
│  Use the `--package-manager` flag to select a different package manager.       │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  checkout-practice-app is ready for you to build!                              │
│                                                                                │
│  Next steps                                                                    │
│    • Run `cd checkout-practice-app`                                            │
│    • For extensions, run `shopify app generate extension`                      │
│    • To see your app, run `shopify app dev`                                    │
│                                                                                │
│  Reference                                                                     │
│    • Shopify docs [1]                                                          │
│    • For an overview of commands, run `shopify app --help`                     │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://shopify.dev
  

Checkout UI Extension App

Next step is to create the checkout UI extension. This is the code that will modify the checkout page. Change to you project directory and run npm run generate extension. You can choose what technology stack you'ld like to work in. I have chosen JavaScript React for this example.

  
C:\shopify\checkout-practice-app>npm run generate extension

> checkout-practice-app@1.0.0 generate
> shopify app generate extension


Before proceeding, your project needs to be associated with an app.

?  Create this project as a new app on Shopify?
√  Yes, create it as a new app

?  App name:
√  checkout-practice-app

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

?  Type of extension?
√  Checkout UI

?  Name your extension:
√  free-shirt-1000

?  What would you like to work in?
√  JavaScript React


╭─ success ──────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Your extension was created in extensions/free-shirt-1000.                     │
│                                                                                │
│  Next steps                                                                    │
│    • To preview this extension along with the rest of the project, run `npm    │
│      run shopify app dev`                                                      │
│                                                                                │
│  Reference                                                                     │
│    • For more details, see the docs [1]                                        │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
[1] https://shopify.dev/api/checkout-extensions/checkout/configuration
  

Take a look at your Shopify Partners portal page and your app should be there.

Checkout UI Extension Logic

Checkout.jsx

  
import {
  BlockLayout,
  InlineLayout,
  Text,
  useTranslate,
  reactExtension,
  useSettings,
  useSubtotalAmount,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
  'purchase.checkout.cart-line-list.render-after',
  () => ,
);

function Extension() {
  const translate = useTranslate();
  const { freebie_title: freeItem } = useSettings();
  const { amount } = useSubtotalAmount();

  if (freeItem && amount > 1000) {
    return (
      <InlineLayout columns={['fill', '20%']}>
          <Text>{ freeItem }</Text>
          <BlockLayout inlineAlignment="end">
              <Text>Free</Text>
          </BlockLayout>
      </InlineLayout>
    );
  }  
}
  

What does this piece of code do? This UI extension targets purchase.checkout.cart-line-list.render-after. Which means this UI will be inserted at that target location. It will be rendered after all line items.

Moving along, we use 3 APIs provided by Shopify. The useTranslate hook returns the I18nTranslate interface used to translate strings. We can do something like <Text>{translate('welcome')}</Text> which pulls the welcome message in en.default.json and renders it in a Text component. But we won't be using it in this example.

The useSettings hook returns the settings values defined by the merchant for the extension. These settings values are found in shopify.extension.toml. The value of the key freebie_title is then assigned to freeItem. We can set the value in the customize mode of the checkout page which is driven by the below extension settings.

shopify.extension.toml

  
...snipped...

[extensions.settings]
  [[extensions.settings.fields]]
    key = "freebie_title"
    type = "single_line_text_field"
    name = "Freebie title"
    description = "Free item name"
  

The useSubtotalAmount API returns a Money value representing the subtotal value of the items in the cart at the current step of checkout. Obviously we'll need the amount to check if it is more than a thousand so we can render the free item. So if there is a free item set and the subtotal is more than a thousand we render the free item using Shopify provided React components. This ends the coding part.

Running and Customizing the App

Time to run the app. Execute npm run dev and follow the instructions (e.g. "P" to preview in your browser).

  
C:\shopify\checkout-practice-app>npm run dev

> checkout-practice-app@1.0.0 dev
> shopify app dev

?  Which store would you like to use to view your project?
√  checkout-practice

╭─ info ─────────────────────────────────────────────────────────────────────────╮
│                                                                                │
│  Using shopify.app.toml:                                                       │
│                                                                                │
│    • Org:             Joel Patrick Llosa                                       │
│    • App:             checkout-practice-app                                    │
│    • Dev store:       checkout-practice.myshopify.com                          │
│    • Update URLs:     Not yet configured                                       │
│                                                                                │
│   You can pass `--reset` to your command to reset your app configuration.      │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

✔ Created extension free-shirt-1000.
07:43:25 │ graphiql   │ GraphiQL server started on port 3457
07:43:25 │ extensions │ Bundling UI extension free-shirt-1000...
07:43:25 │ extensions │ Parsed locales for extension free-shirt-1000 at
C:/shopify/checkout-practice-app/extensions/free-shirt-1000
07:43:25 │ extensions │ free-shirt-1000 successfully built
07:43:25 │ extensions │ Parsed locales for extension free-shirt-1000 at
C:/shopify/checkout-practice-app/extensions/free-shirt-1000
07:43:27 │ extensions │ Draft updated successfully for extension: free-shirt-1000

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
› Press d │ toggle development store preview: ✔ on
› Press g │ open GraphiQL (Admin API) in your browser
› Press p │ preview in your browser
› Press q │ quit

Preview URL: https://damaged-breakfast-campus-syria.trycloudflare.com/extensions/dev-console
GraphiQL URL: http://localhost:3457/graphiql
  

Before we can see our app in action, we need to do some bits and bobs. Go to Settings > Checkout > Customize to add the app block.

Add app block. Can you see the free-shirt-1000 extension?

App block added. Can you see it below the line items?

App block settings. Does it remind you of the extension settings in shopify.extension.toml? We're giving away an Acme Brand T-shirt for purchases over a thousand.

Now, go purchase something over a thousand and go to checkout. You should have something like below.

Try buying something that's belowe a thousand and go to checkout. You are not getting a free item.

Shopify Checkout UI Extension Summary

There you have it. Your first Shopify Checkout UI Extension. To recap, create the scaffolding using Shopify CLI. After that is in place generate an extension. Choose a target location in the checkout page and add your custom logic. Last bit is doing some bit of config to render your UI extension. Good luck creating your own Shopify Checkout UI Extension in the future. Grab the repo here, github.com/jpllosa/checkout-practice-app.

Saturday, March 23, 2024

Copy Shopify Product Example

If you are working on a Shopify project, chances are you'll have multiple stores. One for development, one for pre-production and one for production or live which is accessible to the public. Because of this, you'ld probably want these stores to have the same products. Although there is an option to import/export products in Shopify admin, it does not copy over the metafields. And you're in deep equine effluence if rely heavily on metafields.

In this example, we are going to learn how to copy a product to another store using Shopify GraphQL. This example builds upon my previous blog, Shopify Bulk Query Example. I would recommend you read the previous blog if you want to know more about the cached products. I will only talk about how to copy a product to another store in this article. Okay, let's get to it.

Target Store

As mentioned above, we are building upon the previous example, Shopify Bulk Query Example. So we already have a Quickstart store in place. Let's create a target development store. On the Shopify partners page go to Stores -> Add store -> Create development store. Choose "Create a store to test and build", fill in the form as appropriate. Example Copy Product store:

After the target store has been created, it shouldn't have any products like below.

Custom App on Target Store

On the target store, go to Settings -> Apps and sales channels -> Develop apps. Allow custom app development and Create an app. Let's name it Product API. On the Configuration tab you must have the access scopes write_products and read_products. As of this writing, the webhook version is 2024-01. Install the app. On the API credentials tab, take note of your Admin API access token (you'll need to add this in the request header). Keep your access tokens secure. Only share them with developers that you trust to safely access your data. Take note of your API key and secret key as well. Our Copy Product store is now configured and ready to create products. For detailed steps on how to create a custom app, please take a look at my previous blog, Consuming Shopify Product API Example.

Config Changes

First off, we'll need to add the target store to our config file and update our code. Should be self explanatory based on the names. We have a source store and a target store. We copy the product from the source store (i.e. endpoint) over to the target store (i.e. targetEndpoint)

example-config.json


{
    "port": 0,
    "shopify": {
        "endpoint": "https://.myshopify.com/admin/api//graphql.json",
        "accessToken": "yourAccessToken",
        "targetEndpoint": "https://.myshopify.com/admin/api//graphql.json",
        "targetAccessToken": "targetAccessToken"
    }
}

config.go


// snipped...

type Shopify struct {
	Endpoint          string `json:"endpoint"`
	AccessToken       string `json:"accessToken"`
	TargetEndpoint    string `json:"targetEndpoint"`
	TargetAccessToken string `json:"targetAccessToken"`
}

// snipped...

Routing Addition

In main.go, we add the route to /copy-product which we will hit to trigger the copying of a product.

main.go


    // snipped...

    r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))
	r.Mount("/cached-products", router.GetCachedProducts(config, products))
	r.Mount("/copy-product", router.CopyProduct(config, products))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

Copy Shopify Product Handler

In this part of the code, we pull the gid of the product that we are going to copy from the query parameter. We then find it if that product exists in the product cache. We call the CopyProduct function which returns an error if the copy was unsuccessful and then we send back a Copy product failed message to the caller in JSON format. For a successful copy, the source product information is returned.

products.go


// snipped...

func CopyProduct(config config.Config, products []service.Product) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {

		gid := r.URL.Query().Get("gid")

		copyProduct := service.Product{
			ID:          "",
			Title:       "",
			Handle:      "",
			Vendor:      "",
			ProductType: "",
			Tags:        make([]string, 0),
			Metafields:  make([]service.Metafield, 0),
		}

		for _, product := range products {
			if product.ID == gid {
				fmt.Println("Found:", product.ID)
				copyProduct = product
				break
			}
		}

		jsonBytes := marshaller.Marshal(copyProduct)

		err := service.CopyProduct(config, copyProduct)
		if err != nil {
			jsonBytes = []byte("{ \"message\": \"Copy product failed\"}")
		}

		w.Header().Set(contentType, applicationJson)
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

Copy Shopify Product Crux

Nothing fancy. Just an update on the data model based on Shopify documentation. For more information, go to Shopify Docs.

bulk-query.go


// snipped...

type ProductCreate struct {
	Product Product `json:"product,omitempty"`
}

type Data struct {
	BulkOperationRunQuery BulkOperationRunQuery `json:"bulkOperationRunQuery,omitempty"`
	CurrentBulkOperation  BulkOperation         `json:"currentBulkOperation,omitempty"`
	ProductCreate         ProductCreate         `json:"productCreate,omitempty"`
}

// snipped...

This is where all the grunt work happens. The first lines of the copy product function will create the metafields portion of the GraphQL query. The metafields portion is then inserted to the productCreate GraphQL mutation query. For this example, we are going to assume all metafields are of type single_line_text_field. Because of this, the caveat is, this will not work if the metafield of the product you are going to copy is of a different type. The request is then sent to the target endpoint with the target accss token in the header. If all goes well, it will print the gid of the newly created product. Otherwise the error is passed to the caller to handle.

copy-product.go


package service

import (
	"fmt"
	"io"
	"net/http"
	"shopify-product-api/config"
	"shopify-product-api/marshaller"
	"strings"
)

func CopyProduct(config config.Config, product Product) error {
	// https://shopify.dev/docs/api/admin-graphql/2024-01/mutations/productCreate
	fmt.Println("++ copy product")

	metafieldStr := ""
	var sb strings.Builder

	for i := 0; i < len(product.Metafields); i++ {
		if i > 0 {
			sb.WriteString(",")
		}
		sb.WriteString("{")
		sb.WriteString(fmt.Sprintf(`namespace: "%s",`, product.Metafields[i].Namespace))
		sb.WriteString(fmt.Sprintf(`key: "%s",`, product.Metafields[i].Key))
		sb.WriteString(fmt.Sprintf(`value: "%s",`, product.Metafields[i].Value))
		sb.WriteString(fmt.Sprintf(`type: "%s"`, "single_line_text_field")) // assume all metafields are of this type
		sb.WriteString("}")
	}

	if len(product.Metafields) > 0 {
		metafieldStr = fmt.Sprintf(`metafields: [%s]`, sb.String())
	}

	productCreateGql := fmt.Sprintf(`
	mutation { 
		productCreate(
			input: {
				title: "%s",
				productType: "%s",
				vendor: "%s",
				tags: "%s",
				%s
			}
		) {
			product {
				id
			}
		}
	}
	`, product.Title, product.ProductType,
		product.Vendor, strings.Join(product.Tags, ","),
		metafieldStr)

	query := GqlQuery{
		Query: productCreateGql,
	}

	client := &http.Client{}

	responseBody, err := sendCopyRequest(client, query, config)
	if err != nil {
		return err
	}

	gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

	fmt.Println("New product copied: ", gqlResp.Data.ProductCreate.Product.ID)

	return nil
}

func sendCopyRequest(client *http.Client, query GqlQuery, config config.Config) ([]byte, error) {
	q := marshaller.Marshal(query)
	body := strings.NewReader(string(q))

	req, err := http.NewRequest(http.MethodPost, config.Shopify.TargetEndpoint, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add(contentType, applicationJson)
	req.Header.Add("X-Shopify-Access-Token", config.Shopify.TargetAccessToken)

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	fmt.Println("Response status:", resp.Status)

	return io.ReadAll(resp.Body)
}

Demonstration

I'm assuming you have read my previous blogs, Consuming Shopify Product API Example and Shopify Bulk Query Example. So you should be able to run the golang app and hit the endpoints with Postman. First, let's fetch the cached products and I've chosen gid://shopify/Product/8787070452004 as my source product like so.

Second, let's hit the copy product endpoint and provide the gid query parameter. A successful copy returns the source product information like so.

On the logs, we should see something like below.


Found: gid://shopify/Product/8787070452004
++ copy product
Response status: 200 OK
New product copied:  gid://shopify/Product/8343228219651
2024/03/16 22:35:45 "GET http://localhost:4000/copy-product?gid=gid://shopify/Product/8787070452004 HTTP/1.1" from [::1]:51397 - 200 283B in 22.7344888s

Before we can see the metafields on the Shopify admin product page, we need some bit of configuration for the metafields to show on the product page. Go to Settings -> Custom Data -> Products -> Metafields without a definition. Our namespace and key have been created. Add the definition (i.e. name, description and type). After saving, it will appear in the product page.

Finally, we can see our copied product along with the tags and metafields in all its glory.

Copy Shopify Product Wrap Up

There you have it. A way to copy a product from store to store in Shopify using Shopify GraphQL Admin API. This should form as a base for copying products between stores. Grab the full repo here, github.com/jpllosa/shopify-product-api/tree/copy-product.

Monday, December 25, 2023

Shopify Bulk Query Example

When should I use bulk query? Read on and let's analyse it together.

This blog builds upon my previous blog, Consuming Shopify Product API Example. We'll compare the bulk query with the non-bulk query we did on that example.

Shopify Bulk Query Changes

These are the changes we made to the previous example so we can do Shopify bulk query.

main.go


// snipped...

func main() {
	config, err := config.Load("./conf/config.json")
	if err != nil {
		panic(err)
	}

	products := service.BulkQuery(config)

	r := chi.NewRouter()
	r.Use(middleware.Logger)

	r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))
	r.Mount("/cached-products", router.GetCachedProducts(config, products))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

Just a few changes here. We created a new package named service to handle the bulk query operations and a new path to hit to pull the cached products. Quick and easy.

model.go


package service

type Node struct {
	ID          string   `json:"id,omitempty"`
	Title       string   `json:"title,omitempty"`
	Handle      string   `json:"handle,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	ProductType string   `json:"producType,omitempty"`
	Tags        []string `json:"tags,omitempty"`
	Namespace   string   `json:"namespace,omitempty"`
	Key         string   `json:"key,omitempty"`
	Value       string   `json:"value,omitempty"`
	ParentID    string   `json:"__parentId,omitempty"`
}

type Product struct {
	ID          string      `json:"id,omitempty"`
	Title       string      `json:"title,omitempty"`
	Handle      string      `json:"handle,omitempty"`
	Vendor      string      `json:"vendor,omitempty"`
	ProductType string      `json:"producType,omitempty"`
	Tags        []string    `json:"tags,omitempty"`
	Metafields  []Metafield `json:"metafields,omitempty"`
}

type Metafield struct {
	Namespace string `json:"namespace,omitempty"`
	Key       string `json:"key,omitempty"`
	Value     string `json:"value,omitempty"`
	ParentID  string `json:"__parentId,omitempty"`
}

Here we model the data types we use.

products.go


// snipped...
func GetCachedProducts(config config.Config, products []service.Product) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {

		jsonBytes := marshaller.Marshal(products)

		w.Header().Set(contentType, applicationJson)
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

A function that handles requests to the new /cached-products path.

bulk-query.go


// snipped...
func BulkQuery(config config.Config) []Product {
	fmt.Println("++ bulk query")
	bulkQueryGql := fmt.Sprintf(`
	mutation {
		bulkOperationRunQuery(
			// ... snipped ...
		}
	}
	`)

	query := GqlQuery{
		Query: bulkQueryGql,
	}

	client := &http.Client{}

	responseBody, err := sendRequest(client, query, config)
	if err != nil {
		panic(err)
	}

	gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

	if gqlResp.Data.BulkOperationRunQuery.BulkOperation.Status == "CREATED" {
		fmt.Println("Created at: ", gqlResp.Data.BulkOperationRunQuery.BulkOperation.CreatedAt)
		currentOperationQueryGql := fmt.Sprintf(`
		query CurrentBulkOperation {
			currentBulkOperation {
				completedAt
				createdAt
				errorCode
				fileSize
				id
				objectCount
				status
				url
			}
		}
		`)

		query = GqlQuery{
			Query: currentOperationQueryGql,
		}

		for {
			time.Sleep(time.Second * 2)

			responseBody, err := sendRequest(client, query, config)
			if err != nil {
				panic(err)
			}

			gqlResp = marshaller.Unmarshal[GqlResponse](responseBody)

			if gqlResp.Data.CurrentBulkOperation.Status == "CANCELED" ||
				gqlResp.Data.CurrentBulkOperation.Status == "CANCELING" ||
				gqlResp.Data.CurrentBulkOperation.Status == "EXPIRED" ||
				gqlResp.Data.CurrentBulkOperation.Status == "FAILED" {
				fmt.Println("Status: ", gqlResp.Data.CurrentBulkOperation.CreatedAt)
				break
			}

			if gqlResp.Data.CurrentBulkOperation.Status == "COMPLETED" {
				fmt.Println("URL: ", gqlResp.Data.CurrentBulkOperation.URL)
				productFile, err := downloadFile("products.tmp", gqlResp.Data.CurrentBulkOperation.URL)
				if err != nil {
					break
				}
				return parseProductsFile(productFile)
			}
		}
	}

	return make([]Product, 0)
}
// snipped...

This is where all the magic happens. We issue a bulk query request via mutation operation. We then unmarshall the response and check the bulk operation status if it has been created. If it was created, we then poll the current bulk operation until it is completed, canceled, failed, etc. Once it is completed, we download it and save it into a temporary file. This file will be in JSONL (JSON Lines) format, then we will have to parse the file in order to build the product tree.

There is also a webhook way of checking the bulk operation status. It is recommended over polling as it limits the number of redundant API calls. But for the purposes of this example, we'll do polling.

For more details about Shopify bulk operations, go to Perform bulk operations with the GraphQL Admin API. Go to Bulk Operation Status to learn more valid status values.

The JSONL file would look something like below:


{"id":"gid:\/\/shopify\/Product\/8787070189860","title":"The Videographer Snowboard","handle":"the-videographer-snowboard","vendor":"Quickstart (5cec88e7)","productType":"","tags":[]}
{"id":"gid:\/\/shopify\/Product\/8787070222628","title":"The Minimal Snowboard","handle":"the-minimal-snowboard","vendor":"Quickstart (5cec88e7)","productType":"","tags":[]}
{"id":"gid:\/\/shopify\/Product\/8787070386468","title":"The Archived Snowboard","handle":"the-archived-snowboard","vendor":"Snowboard Vendor","productType":"","tags":["Archived","Premium","Snow","Snowboard","Sport","Winter"]}

Running the Shopify Bulk Query Example

On start up, you should see somethingl like below. Shopify has returned with a download URL. Which means the bulk query has completed and we are ready to serve the cached products.

Comparison with Non-Bulk Query

Now let's compare the new way of pulling data to the old way.

Can you spot the difference? In terms of speed, the response time of the new way was clearly super fast. Just 4ms compared to 279ms, imagine that? What's more is that not only did the old way take longer, it returned less data. It return 639 B compared to 4.65 KB. In other words, in the old way we only received 3 products while in the new way we received all products including metafields. That's an icing on the cake. As for start up time of the app, it was negligible.

Shopify Bulk Query Wrap Up

Would you do bulk query now or not? It is up to you to identify a potential bulk query. Queries that use pagination to get all pages of results are the most common candidates. There are limitations on a bulk query though. For example, you can't pull data that's nested two levels deep. Check the Shopify documentation for more information.

There you have it. Another way to pull product data from the Shopify GraphQL Admin API. If you got a better way of doing things (e.g. how to parse the JSONL better), just raise a pull request. Happy to look at it. Grab the repo here, github.com/jpllosa/shopify-product-api, it's on the bulk-query branch.

Saturday, October 28, 2023

Consuming Shopify Product API Example

In this blog, we are going to learn how to pull product data from the Shopify API. To build upon previous examples like Starting Out on the go-chi Framework and GraphQL with Postman, we will implement this in Go using the go-chi framework and hit the Shopify GraphQL endpoint. Let's begin.

User Story

As a user, I want to retrieve my Shopify products in JSON format, so I can use it in my analytics software.

Shopify Setup

First off, I will not go into too much detail in setting Shopify up. I will let the Shopify documentation do that for me. To get this example running, you'll need a Shopify Partner account. When I created my Shopify partner account, it was free and I don't see any reason that it won't be in the future. Once you have a pertner account, create a development store or use the quickstart store with test data preloaded.

Go into your store, then Settings, then Apps and sales channels, then Develop apps and then Create an app.

On the Configuration tab you must have the access scopes write_products and read_products. As of this writing, the webhook version is 2023-10.

On the API credentials tab, take note of your Admin API access token (you'll need to add this in the request header). Keep your access tokens secure. Only share them with developers that you trust to safely access your data. Take note of your API key and secret key as well.

There you have it. We are all set on the Shopify configuration end.

Testing the Shopify API

Before we go straight into coding, let's test the API call first. Fire up Postman and let's hit the GraphQL endpoint. The URL is of the form https://<storename>.myshopify.com/admin/api/<version>/graphql.json. Add X-Shopify-Access-Token in your header and the value is your Admin API access token. We'll do a query for the first 5 products. The image below shows we can happily make a request. Go to GraphQL Admin API reference for more details.


query {
  products(first: 5, reverse: false) {
    edges {
      node {
        id
        title
        handle
        vendor
        productType
        tags
      }
    }
  }
}

The Main

main.go


// snipped...

func main() {
	config, err := config.Load("./conf/config.json")
	if err != nil {
		panic(err)
	}

	r := chi.NewRouter()
	r.Use(middleware.Logger)

	r.Get("/", router.GetStatus)

	r.Mount("/products", router.GetProducts(config))

	http.ListenAndServe(fmt.Sprintf(":%v", config.Port), r)
}

The first thing the program does is load up the configuration. As mentioned above, we are building upon a previous example, Starting Out on the go-chi Framework. We utilize the go-chi framework for routing incoming HTTP reqeusts. Any requests to the root resource will return a status in JSON format. Any requests to the /products resource will return a list of products in JSON format. I will not expand on router.GetStatus because I have explained it in my previously mentioned blog, Starting Out on the go-chi Framework. I just moved it into a new package making it more organized.

Configuration

config.go


// snipped...

type Config struct {
	Port    int     `json:"port"`
	Shopify Shopify `json:"shopify"`
}

type Shopify struct {
	Endpoint    string `json:"endpoint"`
	AccessToken string `json:"accessToken"`
}

func Load(filePath string) (Config, error) {
	config, err := unmarshalFile[Config](filePath)
	if err != nil {
		return Config{}, err
	}

	return config, nil
}

func unmarshalFile[T any](filePath string) (T, error) {
	pointer := new(T)

	f, err := os.Open(filePath)
	if err != nil {
		return *pointer, fmt.Errorf("opening config file: %w", err)
	}
	defer f.Close()

	jsonBytes, err := io.ReadAll(f)
	if err != nil {
		return *pointer, fmt.Errorf("reading config file: %w", err)
	}

	err = json.Unmarshal(jsonBytes, &pointer)
	if err != nil {
		return *pointer, fmt.Errorf("unmarshalling config file: %w", err)
	}

	return *pointer, nil
}

Making the configuration file in JSON format will make it easy for us to use. We simply read the file and unmarshal the byte array into our Config type and bob's your uncle. Then we easily get the value accessing the fields as seen in main.go(e.g. fmt.Sprintf(":%v", config.Port)).

JSON Marshaller

json.go


func Marshal(v any) []byte {
	jsonBytes, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}

	return jsonBytes
}

func Unmarshal[T any](data []byte) T {
	var result T

	err := json.Unmarshal(data, &result)
	if err != nil {
		panic(err)
	}

	return result
}

Nothing special with this file. It is just a wrapper of the standard JSON Go library. Didn't want to muddy the resource handlers with JSON error handling.

Get Products

products.go


// snipped...
type GqlQuery struct {
	Query string `json:"query"`
}

type GqlResponse struct {
	Data       Data       `json:"data,omitempty"`
	Extensions Extensions `json:"extensions,omitempty"`
}

type Data struct {
	Products Products `json:"products,omitempty"`
}

type Products struct {
	Edges []Edge `json:"edges,omitempty"`
}

type Edge struct {
	Node Node `json:"node,omitempty"`
}

type Node struct {
	Id          string   `json:"id,omitempty"`
	Title       string   `json:"title,omitempty"`
	Handle      string   `json:"handle,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	ProductType string   `json:"productType,omitempty"`
	Tags        []string `json:"tags,omitempty"`
}

type Extensions struct {
	Cost Cost `json:"cost,omitempty"`
}

type Cost struct {
	RequestedQueryCost int            `json:"requestedQueryCost,omitempty"`
	ActualQueryCost    int            `json:"actualQueryCost,omitempty"`
	ThrottleStatus     ThrottleStatus `json:"throttleStatus,omitempty"`
}

type ThrottleStatus struct {
	MaximumAvailable   float32 `json:"maximumAvailable,omitempty"`
	CurrentlyAvailable int     `json:"currentlyAvailable,omitempty"`
	RestoreRate        float32 `json:"restoreRate,omitempty"`
}

func GetProducts(config config.Config) chi.Router {
	router := chi.NewRouter()

	router.Get("/", func(w http.ResponseWriter, r *http.Request) {
		productsGql := fmt.Sprintf(`{
			products(first: 3, reverse: false) {
			  edges {
				node {
				  id
				  title
				  handle
				  vendor
				  productType
				  tags
				}
			  }
			}
		  }`)

		query := GqlQuery{
			Query: productsGql,
		}

		q := marshaller.Marshal(query)
		body := strings.NewReader(string(q))

		client := &http.Client{}
		req, err := http.NewRequest(http.MethodPost, config.Shopify.Endpoint, body)
		if err != nil {
			panic(err)
		}
		req.Header.Add("Content-Type", "application/json")
		req.Header.Add("X-Shopify-Access-Token", config.Shopify.AccessToken)

		resp, err := client.Do(req)
		if err != nil {
			panic(err)
		}
		defer resp.Body.Close()

		fmt.Println("Response status:", resp.Status)

		responseBody, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			panic(err)
		}

		gqlResp := marshaller.Unmarshal[GqlResponse](responseBody)

		jsonBytes := marshaller.Marshal(gqlResp.Data.Products.Edges)

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(200)
		w.Write(jsonBytes)
	})

	return router
}

Welcome to the heart of this article. We start by creating the type struct of the GraphQL response so we can easily unmarshal it. As you saw earlier in Testing the Shopify API, the Postman image shows a snapshot of the GraphQL response from Shopify. When GetProducts is called, it starts by creating the GraphQL query for products. Marshal the query into a byte array then passes it into a new HTTP request. We create an HTTP client so we can add in the Shopify Access Token in the header and the content type. We then send the request by calling client.Do(req) and read the response. Converting the response into a byte array so we can unmarshal it. After that we strip out the stuff we don't need. We only need the Nodes. Once we have the nodes, we convert it into bytes and send it back. That is all there is to it. Oh yeah, don't forget to close response after using it.

Output

I've set my port to 4000 so hitting http://localhost:4000 will return a response like below:


{
    "id": 1,
    "message": "API ready and waiting"
}

Hitting http://localhost:4000/products will return a response like below:


[
    {
        "node": {
            "id": "gid://shopify/Product/8787070189860",
            "title": "The Videographer Snowboard",
            "handle": "the-videographer-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070222628",
            "title": "The Minimal Snowboard",
            "handle": "the-minimal-snowboard",
            "vendor": "Quickstart (5cec88e7)"
        }
    },
    {
        "node": {
            "id": "gid://shopify/Product/8787070386468",
            "title": "The Archived Snowboard",
            "handle": "the-archived-snowboard",
            "vendor": "Snowboard Vendor",
            "tags": [
                "Archived",
                "Premium",
                "Snow",
                "Snowboard",
                "Sport",
                "Winter"
            ]
        }
    }
]

Consuming Shopify Product API Wrap Up

There you have it. A way to pull data from the Shopify GraphQL Admin API and spitting it out in a slightly different format. In between we utilized the go-chi framework for routing requests. We used the standard Go library for JSON manipulation, HTTP communication and file access. Lastly, did we satisfy the user story? Grab the full repo here, github.com/jpllosa/shopify-product-api.