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.
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:
- Go to Settings > Plugins > Shopify in the Gadget editor
- Edit an existing connection or set up a new Shopify connection
- Include the
read_customers
access scope in your connection - 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.
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 toaccessControl/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 toshopifyCustomer
sosession
records can be created for customers making requests - A customer account login trigger is added to the
shopifyCustomer
model'screate
action so ashopifyCustomer
record can be automatically retrieved from Shopify if the record does not already exist in your app's database
Customer account authentication is also supported by Gadget's source control system. Enabling it will set customerAuthenticationEnabled:
true
in a project's settings.gadget.ts
file. Note that this file is only available when using ggt
, the Gadget
CLI, to pull your project down to your local machine.
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:
- Add a relationship between your custom model and the
shopifyShop
model so that your custom model belongs toshopifyShop
- Add a relationship between your custom model and the
shopifyCustomer
model so that your custom model belongs toshopifyCustomer
- Add the
preventCrossShopDataAccess
function to therun
function of your custom model's actions - 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:
1import { applyParams, save, ActionOptions } from "gadget-server";2// add `preventCrossShopDataAccess` to the import statement3import { preventCrossShopDataAccess } from "gadget-server/shopify";45export const run: ActionRun = async ({6 params,7 record,8 logger,9 api,10 connections,11}) => {12 applyParams(params, record);13 // add this line to prevent customers from accessing other customers' data14 await preventCrossShopDataAccess(params, record);15 await save(record);16};1718export const options: ActionOptions = {19 actionType: "create",20};
1import { applyParams, save, ActionOptions } from "gadget-server";2// add `preventCrossShopDataAccess` to the import statement3import { preventCrossShopDataAccess } from "gadget-server/shopify";45export const run: ActionRun = async ({6 params,7 record,8 logger,9 api,10 connections,11}) => {12 applyParams(params, record);13 // add this line to prevent customers from accessing other customers' data14 await preventCrossShopDataAccess(params, record);15 await save(record);16};1718export const options: ActionOptions = {19 actionType: "create",20};
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 modelgellyfilter ($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:
await preventCrossShopDataAccess(params, record, { enforceCustomerTenancy: false });
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:
- Set up your Gadget app to work with Shopify extensions and generate a new customer account UI extension
- Install the
@gadgetinc/shopify-extensions
package inside theextensions/<your-extension-name>
folder:
yarn add @gadgetinc/shopify-extensions
If you are using a separate Shopify CLI project to build extensions, you will also need to install the @gadgetinc/react
package.
- Initialize a new client, and wrap the extension component with the
Provider
component from@gadgetinc/shopify-extensions
and pass it theclient
andsessionToken
:
1import { Client } from "@gadget-client/example-app";2import { useFindOne } from "@gadgetinc/react";3import { Provider, useGadget } from "@gadgetinc/shopify-extensions/react";4import {5 Button,6 reactExtension,7 useApi,8} from "@shopify/ui-extensions-react/customer-account";910// initialize a new Client for your Gadget API11const client = new Client();1213// the Provider is set up in the reactExtension() initialization function14export default reactExtension(15 "customer-account.order.action.menu-item.render",16 () => <GadgetUIExtension />17);1819// component to set up the Provider with the sessionToken from Shopify20function GadgetUIExtension() {21 const { sessionToken } = useApi();2223 return (24 <Provider api={client} sessionToken={sessionToken}>25 <MenuActionExtension />26 </Provider>27 );28}2930function MenuActionExtension() {31 // get the 'api' client and a 'ready' boolean from the useGadget hook32 const { api: gadgetApi, ready } = useGadget<Client>();33 // get an instance of the Shopify API object34 const api = useApi();3536 // the useFindOne hook gets the issueId (a custom field on order)37 // of the current order from the Gadget API38 const [{ data: order, fetching, error }] = useFindOne(39 gadgetApi.shopifyOrder,40 api.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 requests46 pause: !ready,47 }48 );4950 // do not display anything if the data is still being fetched or there is an error51 if (fetching) return null;52 if (error) {53 console.error(error);54 return null;55 }5657 // disable the button if an issue has already been reported58 return (59 <Button disabled={!!order?.issueId}>60 {order?.issueId ? "Report submitted" : "Report a problem"}61 </Button>62 );63}
1import { Client } from "@gadget-client/example-app";2import { useFindOne } from "@gadgetinc/react";3import { Provider, useGadget } from "@gadgetinc/shopify-extensions/react";4import {5 Button,6 reactExtension,7 useApi,8} from "@shopify/ui-extensions-react/customer-account";910// initialize a new Client for your Gadget API11const client = new Client();1213// the Provider is set up in the reactExtension() initialization function14export default reactExtension(15 "customer-account.order.action.menu-item.render",16 () => <GadgetUIExtension />17);1819// component to set up the Provider with the sessionToken from Shopify20function GadgetUIExtension() {21 const { sessionToken } = useApi();2223 return (24 <Provider api={client} sessionToken={sessionToken}>25 <MenuActionExtension />26 </Provider>27 );28}2930function MenuActionExtension() {31 // get the 'api' client and a 'ready' boolean from the useGadget hook32 const { api: gadgetApi, ready } = useGadget<Client>();33 // get an instance of the Shopify API object34 const api = useApi();3536 // the useFindOne hook gets the issueId (a custom field on order)37 // of the current order from the Gadget API38 const [{ data: order, fetching, error }] = useFindOne(39 gadgetApi.shopifyOrder,40 api.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 requests46 pause: !ready,47 }48 );4950 // do not display anything if the data is still being fetched or there is an error51 if (fetching) return null;52 if (error) {53 console.error(error);54 return null;55 }5657 // disable the button if an issue has already been reported58 return (59 <Button disabled={!!order?.issueId}>60 {order?.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. It reads a custom issueId
field on the shopifyOrder
model.
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.
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.
If you are using ggt
, the Gadget CLI, to pull your Gadget project down to your local machine, you can also disable customer account authentication by setting customerAuthenticationEnabled: false
in your project's settings.gadget.ts
file.
Removing customer account authentication will automatically remove the customer account login trigger from api/models/shopifyCustomer/actions/create.js
.