[Feature Request] useScopesHook

Hi there,

Loving the new AppBridge functionality, especially the ability to incrementally ask for additional scopes through the app.scopes.request function.

It’s also great to be able to check the initial scopes given the app.scopes.query function.

However, there’s a merchant UX problem.

The issue

You can use app.scopes.query() on component load to check the current scopes, then conditionally if any are missing then use app.scopes.request(missing_scopes) to request them.

The result is that the merchant sees a new nice modal to explain the changes with a button to accept them.

The problem is that after the customer clicks this button, the app has no hook to understand the scopes have changed. Nor is there a hook for when the merchant closes this modal.

This means that as an app developer, we have to poll the current access scopes continuously in order to recognize the state change and update our UI’s in tandem.

Proposal

I propose that a new hook is added to the AppBride specifically for subscribing to when merchants accept new access scopes from this functionality.

Here’s some pseudo code options. For all options, I’m pretending hasAccessGranted is a boolean that controls a gate to ask for additional scopes.

Option 1: Synchronous Access Scopes + useEffect

Currently, querying access scopes is async, which means you can’t subscribe to it in a useEffect.

If this could be switched to a sync op, then we can simply hide/show a gate to accept these permissions.

After the merchant accepts them, the gate will be removed.

const shopify = useAppBridge()
const scopes = shopify.scopes.query

useEffect(() => {
  const isAccessGranted = scopes.includes('write_discounts')

  setAccessGranted(isAccessGranted)
}, [scopes])

Option 2: Dedicated hook

This option exposes a hook that you can use to update local state based on the access scope change:

const shopify = useAppBridge()

// This hook only fires when access scopes have changed and passes the newest version of the scopes as an argument to a callback.
shopify.scopes.onChange((newScopes) => {
  const isAccessGranted = newScopes.includes('write_discounts')
  setAccessGranted(isAccessGranted)
})

These are just suggestions obviously, I’m sure y’all can come up with a better DX. But I just wanted to share this gap in the current implementation.

Alternatively, if there’s some kind of workaround I don’t know about I would appreciate a clue. Right now I don’t see any alternative besides polling or the merchant has to guess to refresh the page to reload the state of their access scopes.

1 Like

Hey hey @Dylan :waving_hand: - this is a great idea. Can’t guarantee anything specific, but I’ll set up a feature request report on my end here and make sure it gets escalated to the right folks - thanks for taking the time to write this up, really appreciated! Let me know if I can help out further in the thread here.

:waving_hand: Hello Dylan! Thank you for your thoughtful inquiry and ideas that you have proposed here.

The App Bridge Scopes API should already have what you need to tackle this. There may be some other tools we can work with as well. Let’s take a look.

Purposefully asynchronous API

The Scopes API is purposefully async. A couple reasons for this:

  • For request, the promise is not resolved until the merchant takes an action
  • Shopify returns a payload of fresh, relevant data from each of query, request, and revoke

The response data is structured and typed. You can see what is returned by clicking on the response type buttons on the docs page (e.g. for Promise<ScopesDetail>, click on “ScopesDetail”).

A couple relevant points worth pointing out about response data:

  • each of query, request, and revoke returns ScopesDetail, which gives you an up-to-date view of what scopes have been granted by the merchant
  • request also responds with UserResult, which tells you what action the merchant took ('granted-all' | 'declined-all'). If you call request() for scopes that have already been granted, we’ll still return granted-all.

Here’s a screenshot of all the response types expanded:

Understanding that scopes have changed

We don’t want to be too prescriptive here on implementation — partly because different apps are going to have different needs in this area — but the general idea is that you can await the response from scopes.request() and then work with the data that is returned to you. For example, as a rough sketch, you could:

// given an array of granted scopes, checks if a feature has what it needs
function hasScopesForMyFeature(grantedScopes) {
  // evaluate if MyFeature's scopes are all found in the grantedScopes
  // return boolean
}

async function handleMyRequestButtonClick() {
  const requestResult = await scopes.request(scopesForMyFeature);

  if (hasScopesForMyFeature(requestResult.detail.granted)) {
    setAccessGranted(true);
  }
}

A rough sketch, but wanting to show that you can await for the data, evaluate it, then go update app state based on what the merchant has granted.

Having a fresh update on the scopes that the merchant has granted, following merchant action, should unlock a number of ways to manage application state.

Not all apps will have this complex of a setup. A simpler, related approach is described below.

A little side note / tip: last winter we added the APP_SCOPES_UPDATE webhook that can be subscribed to. This can also be used for updating your app’s knowledge of the current installation.

Knowing about the merchant’s response to the modal

Similar to the above, we have a rough sketch in the documentation for this. See the example usage of the request method here. You can confirm that the merchant granted scopes with ~ response.result === 'granted-all', and then update app state based on that.

You’ll also know if a merchant has cancelled/closed/rejected the scope request with 'declined-all', so you can conditionally render some content letting them know that they need to grant access in order to proceed, provide them further context, etc.

For simpler scope request flows, we’ve found this to be a helpful pattern. However, we know that not all flows will be simple, so the request response also includes detail: ScopesDetail for more complex evaluations; see the section above.

Please give us feedback!

When designing the Scopes API, we prioritized the topics raised here — aiming to ensure that developers have the information they need to update application state and make informed decisions about what to show merchants when.

With that said, I’d like to heartily welcome feedback on the APIs, as well as our documentation. Would love to hear from you Dylan & any other folks on how the APIs are working out for you, as well as any blockers, hurdles, or unclear documentation that you’ve faced.

Oh great, I didn’t realize that shopify.scopes.request returned anything at all. That’s even better.

That solves this real time feedback problem. Perfect, I’ll try this out now.

A little side note / tip: last winter we added the APP_SCOPES_UPDATE webhook that can be subscribed to. This can also be used for updating your app’s knowledge of the current installation.

Right, but the problem with webhooks is twofold:

  1. App UIs aren’t naturally connected to webhooks, you’d have to implement websockets/server side events in order to sync this feedback to the merchant
  2. Webhooks aren’t guaranteed in the first place. Nice for real time-ish updates, but when scopes are blocking a merchant from completing a task in real time, it’s not an optimal option.

Don’t get more wrong, I appreciate the focus put into webhooks, it’s definitely a great addition especially for events like when merchants upgrade to OSP/TYP checkout profiles.

Maybe more practical guides on how to use these more advanced AppBridge functionality is what would help communicate these APIs better. But the answer is still ultimately in the API documentation, you just have to read closely.