Customer account authentication & customer account UI extensions 

What are customer account UI extensions? 

Shopify customer account extensions allow developers to build custom apps and UIs in Shopify's order status portal. Details about the new customer extensibility features can be found in the Shopify docs.

To build a customer account UI extension with Gadget, you need to make sure that requests made to your Gadget API from the extension are secure, and that the customer can only access their own data.

Limited to new customer accounts

Shopify's customer account UI extensions and customer authentication in Gadget are currently only available to stores using Shopify's new customer accounts. Classic customer accounts are not supported.

Enable customer account authentication 

To set up your Gadget API to handle requests from the customer account UI extension:

  1. Go to Settings > Plugins > Shopify in the Gadget editor
  2. Edit an existing connection or set up a new Shopify connection
  3. Include the read_customers access scope in your connection
  4. Enable Customer account authentication and Confirm your changes

The shopifyCustomer data model will be added to your connection configuration automatically if it was not already selected.

Protected customer data access

Customer data requires you to fill out Shopify's protected customer data access form for webhooks to be registered.

What does enabling customer account authentication do? 

Enabling customer account authentication on a Gadget app will make the following changes to your app:

  • Add a new shopify-storefront-customers access role to accessControl/permissions that allows you to manage authorization for customer requests
    • This role is granted read access to any customer data that is stored in the app
    • A tenancy filter is automatically created and enforced so that customers can only read their own data
  • Add a relationship from the session model to shopifyCustomer so session records can be created for customers making requests
  • A customer account login trigger is added to the shopifyCustomer model's create action so a shopifyCustomer record can be automatically retrieved from Shopify if the record does not already exist in your app's database

Customer data tenancy 

In most cases, customers should only access their own data.

To ensure this, Gadget automatically sets up customer data tenancy for the following Shopify models when you enable customer account authentication:

  • shopifyCheckout
  • shopifyCompanyContact
  • shopifyCustomerAddress
  • shopifyCustomerMergeable
  • shopifyCustomerPaymentMethod
  • shopifyDraftOrder
  • shopifyGiftCard
  • shopifyOrder
  • shopifySubscriptionContract

For any other models, including custom models, you will need to manually add customer data tenancy.

Set up customer multi-tenancy manually 

You must do three things to enable multi-tenancy for customers on your custom models:

  1. Add a relationship between your custom model and the shopifyCustomer model
  2. Add the preventCrossShopDataAccess function to the run function of your custom model's actions
  3. Add a tenancy filter to your custom model

Using preventCrossShopDataAccess 

You can use the preventCrossShopDataAccess function, imported from the gadget-server package, to ensure that customers can only access their data in your custom model's actions.

This function will automatically be added to any custom actions created on Shopify data models.

An example of how to use this function in a custom blog model's create action:

api/models/blog/actions/create.js
JavaScript
1// add `preventCrossShopDataAccess` to the import statement
2import {
3 applyParams,
4 save,
5 ActionOptions,
6 CreateBlogActionContext,
7} from "gadget-server";
8import { preventCrossShopDataAccess } from "gadget-server/shopify";
9
10/**
11 * @param { CreateBlogActionContext } context
12 */
13export async function run({ params, record, logger, api, connections }) {
14 applyParams(params, record);
15 // add this line to prevent customers from accessing other customers' data
16 await preventCrossShopDataAccess(params, record);
17 await save(record);
18}
19
20/** @type { ActionOptions } */
21export const options = {
22 actionType: "create",
23};

Adding a tenancy filter 

Adding a tenancy filter to the access control configuration for your custom model will make sure that customers can only access their own data.

These filtered model permissions are written in Gelly, Gadget's data access language.

For example, this Gelly snippet could be used to filter a customerOffer model data by customer. The customerOffer model has a customer relationship field that relates to the shopifyCustomer model.

Example of a tenancy filter for a custom model
gelly
filter ($session: Session) on CustomerOffer [
where customerId == $session.shopifyCustomerId
]

For more information on tenancy filters and filtered model permissions, see the access control guide.

Disabling customer multi-tenancy for a Shopify model 

You may want to disable customer multi-tenancy for a Shopify model but still enforce shop multi-tenancy with preventCrossShopDataAccess.

To do this, set the enforceCustomerTenancy option to false in the preventCrossShopDataAccess function call:

Example of disabling customer multi-tenancy for a Shopify model
JavaScript
await preventCrossShopDataAccess(params, record, { enforceCustomerTenancy: false });

For more information on the preventCrossShopDataAccess function, see the Gadget API reference.

Setting up the Shopify CLI extension 

Now you can set up a Shopify CLI app with a customer account UI extension and make requests to your Gadget API. Follow these steps to set up the CLI app:

  1. Set up your Gadget app to work with Shopify extensions and generate a new customer account UI extension
  2. Install your app's unique API client and @gadgetinc/shopify-extensions inside the extensions/<your-extension-name> folder:
Install dependencies
yarn add @gadget-client/example-app @gadgetinc/shopify-extensions
Building extensions outside of Gadget?

If you are using a separate Shopify CLI project to build extensions, you will also need to install the @gadgetinc/react package.

  1. Initialize a new client, and wrap the extension component with the Provider component from @gadgetinc/shopify-extensions and pass it the api client and sessionToken
extensions/customer-account-ui/src/MenuActionExtension.js
JavaScript
1import { useEffect } from "react";
2import {
3 Button,
4 reactExtension,
5 useApi,
6} from "@shopify/ui-extensions-react/customer-account";
7import { Provider } from "@gadgetinc/shopify-extensions";
8import { useFindOne } from "@gadgetinc/react";
9import { Client } from "@gadget-client/example-app";
10
11// initialize a new Client for your Gadget API
12const api = new Client();
13
14// the Provider is set up in the reactExtension() initialization function
15export default reactExtension(
16 "customer-account.order.action.menu-item.render",
17 () => <GadgetUIExtension />
18);
19
20// component to set up the Provider with the sessionToken from Shopify
21function GadgetUIExtension() {
22 const { sessionToken } = useApi();
23
24 return (
25 <Provider api={api} sessionToken={sessionToken}>
26 <MenuActionExtension />
27 </Provider>
28 );
29}
30
31function MenuActionExtension() {
32 // get the 'api' client and a 'ready' boolean from the useGadget hook
33 const { api, ready } = useGadget();
34 const { orderId } = useApi();
35
36 // the useFindOne hook gets the issueId (a custom field on order)
37 // of the current order from the Gadget API
38 const [{ data: orderIssue, fetching, error }] = useFindOne(
39 api.shopifyOrder,
40 orderId.split("/").pop(),
41 {
42 select: {
43 issueId: true,
44 },
45 // use ready to pause hooks until the API client is ready to make authenticated requests
46 pause: !ready,
47 }
48 );
49
50 // do not display anything if the data is still being fetched or there is an error
51 if (fetching) return null;
52 if (error) {
53 console.error(error);
54 return null;
55 }
56
57 // disable the button if an issue has already been reported
58 return (
59 <Button disabled={!!orderIssue.issueId}>
60 {orderIssue.issueId ? "Report submitted" : "Report a problem"}
61 </Button>
62 );
63}

This is an example of how to use the useFindOne hook to fetch custom data from your Gadget API and use it in your extension.

The Provider component will automatically pass the sessionToken to the api client. This will allow customers to make authenticated requests to your Gadget API, and grants them the shopify-storefront-customers role.

A ready boolean is used to pause hook calls until the api client is ready to make authenticated requests.

Not using React?

If you are not using React to build your extensions, you can still use the @gadgetinc/shopify-extensions package to handle authentication and make requests to your Gadget API. Check out the package docs for more information.

Disabling customer account authentication 

To disable customer account authentication you can edit your existing Shopify connection in the Gadget editor and disable the Customer account authentication setting.

Removing customer account authentication will automatically remove the customer account login trigger from the shopifyCustomer model's create action.