Open zirkelc opened 2 weeks ago
Hey, thanks for raising this. Unfortunately, because Remix calls loaders in parallel and they don't share context, there's no way to guarantee their order, or to have them wait for one another. However, it should still be possible to achieve what you want, even if it is a bit more verbose.
The way the template is set up, any route under /app
uses, as you pointed out:
app/routes/app.tsx
layoutapp/routes/app.<route>.tsx
The main reason we set it up that way was because the layout makes it easier to add more routes, but the layout isn't absolutely necessary, so you can authenticate any route, even if it isn't under /app
.
You should be able to load a route with a single loader
if that route:
AppProvider
component
headers
and ErrorBoundary
I don't know the structure of your routes exactly, but I would imagine you don't have to do this for every one of them, just the ones that will be accessed directly (e.g. the home page or any deep link for a partner coming from outside the app), when a user would actually be created.
[!TIP] If you need to change your home page's route, you should also change this redirect call
Hope this helps!
Hi @paulomarg thank you for taking the time to respond!
I'm not sure I understand your suggestion completely. Let me try to summarize it:
After an app was approved to be installed via the managed installation flow, Shopify redirects the merchant to the app. What is the target URL after in this case? I assume it's the application_url
in the shopify.app.toml
(same asappUrl
on shopifyApp()
), right?
The route (and loader) which is active on this application_url
route must authenticate the request and trigger the afterAuth
hook to create the user in the database. Then, its should redirect to any other routes that assumes the user already exists. So the key is that only one route (loader) after installation is active.
Is this what you meant?
On a sidenote, what is the purpose of the _index/route.tsx
and who calls this route?
After an app was approved to be installed via the managed installation flow, Shopify redirects the merchant to the app. What is the target URL after in this case? I assume it's the application_url in the shopify.app.toml (same asappUrl on shopifyApp()), right?
Right!
The route (and loader) which is active on this application_url route must authenticate the request and trigger the afterAuth hook to create the user in the database. Then, its should redirect to any other routes that assumes the user already exists. So the key is that only one route (loader) after installation is active.
Right again. What happens here is that the template runs 2 loader
s in this case - one in routes/app._index.tsx
, and one in routes/app.tsx
- the second one is a Remix layout that is run for every route under routes/app.*.tsx
.
What I meant was that you don't need to have a layout, so you could for instance delete routes/app.tsx
, and the routing would still work, and only the loader in routes/app._index.tsx
would be run.
That would probably solve the problem you're having with 2 loaders running for that action. The downside is that you would need to replicate the code from routes/app.tsx
in every route in routes/app.*.tsx
for authenticate.admin
to work properly.
Hope that made it a little clearer!
On a sidenote, what is the purpose of the _index/route.tsx and who calls this route?
This is a landing / marketing page that you can customize however you want, if you want to show it to users to encourage them to try the app / other features that aren't app related. You can reach it by visiting https://<your-app-url>
directly from the browser :)
Thanks again for the quick reply, I got it! :) I will try that out and see how it works.
I have two alternative ideas and I'd like to hear your opinion on them:
loader
receives the current session
from authenticate.admin
. let's say there is a session (e.g. created in the last 60 seconds) but no existing user
yet, that could mean the user
is just being created by another loader
. to solve this issue, I could just throw a redirect
to trigger a reload of the current URL. I would just need to know the current URL / route somehow. Do you know if there is a way to get this information?afterAuth()
runs, it could throw a Response
in order to redirect the user to some kind of post-installation route (e.g. to show a welcome message, intro, etc) or to simply trigger a refresh of the current URL and routes. This is currently not possible because the thrown object from afterAuth
is swallowed by this try-catch:
https://github.com/Shopify/shopify-app-js/blob/ed8c30e016d061558dd4321accbcd492524bf569/packages/apps/shopify-app-remix/src/server/authenticate/admin/strategies/token-exchange.ts#L81-L93
Wouldn't it make sense to allow the Response
to bubble up so we have more flexibility of what happens post-auth? I know this try-catch block is embedded in the authenticate
that is supposed to return a session
, so this feature would require some kind of re-write to make it work. On the other side, it looks like authenticate
is being called by the authStrategyFactory
here:
https://github.com/Shopify/shopify-app-js/blob/ed8c30e016d061558dd4321accbcd492524bf569/packages/apps/shopify-app-remix/src/server/authenticate/admin/authenticate.ts#L137-L155
This function authStrategyFactory.authenticateAdmin()
lets a possible Error
or Response
bubble up, so if authenticate
would return a session
and/or a Response
it could just be re-thrown here.Option 1: You can get the URL in a loader by using request.url
, and I think throwing that redirect would work, since AFAIK the first thrown response would cause the request to be returned right away
Option 2: I think that's a fair ask regardless - when we catch the after auth hook, you could throw a Response
instead of an Error
.
I don't really see a reason why we couldn't bubble up responses at that point, it enables the app to do an early return and shouldn't have any consequences for us, as long as that call is always the last action we take during authentication (which it should be anyway).
I think we should do Option 2, but to be transparent it might be a little while before we can get to it. If you feel comfortable changing / testing it, please feel free to contribute a PR and we'll prioritize it.
That would be great! Sure, I'll be happy to work on this and submit a PR in the next few days.
Thanks for your support!
Issue summary
Hi,
I'm not sure if this is strictly a bug or if I simply need some advice how to handle the new managed installation flow correctly. I'm using the default remix template created by the Shopify CLI. After the client has installed the app through managed installation, Shopify redirects the client to the app on route
/app
. Since it's the first request after installation, theshopifyApp()
starts the Authenticating admin request flow and creates a new session and stores it on the session storage. This process is executed multiple times by eachloader()
function matching the current route. In this case, there are two loaders being executed for the request/app
:/app/routes/app.tsx
/app/routes/app._index.tsx
.Both loaders run in parallel due to Remix's parallel route loading. Then, after Authenticating admin request has finished the
afterAuth()
hook is called. This hook would actually be called twice because of each loader. However, the managed installation via theTokenExchangeStrategy
dedupes this call so it only runs once:https://github.com/Shopify/shopify-app-js/blob/7045fe4e3a9c39b858ca8e8337978ee42d496b82/packages/apps/shopify-app-remix/src/server/authenticate/admin/strategies/token-exchange.ts#L159-L172
I use the
afterAuth
hook to create a database entities (user, shop, etc) in my own system. All other authenticated routes need this information and query this API in their respective loaders. Now the problem is that the managed installation throughTokenExchangeStrategy
doesn't provide a sequential authentication flow like with the previousAuthCodeFlowStrategy
. The authentication in a managed installation seem to be started by multiple loaders in parallel, and the "fastest" loader calls theafterAuth
hook. That means there are multiple routes rely on the information being created in theafterAuth
hook but it may not be created yet. So I end up with lots of not found errors in loaders because they run before theafterAuth
has finished. The errors disappear after a reload.My first idea was to throw a redirect at the end of
afterAuth
to force the entire route to reload when the entity was created. Butredirect
is not exposed on the params ofafterAuth
and any error thrown insideafterAuth
it caught by an outertry-catch
anyway.What is the right way to handle this issue? I think doing API calls inside
afterAuth
after installation sounds like a normal thing, right?Before opening this issue, I have:
@shopify/*
package and version:{ logger: { level: LogSeverity.Debug } }
in my configuration, when applicableExpected behavior
After the managed installation, all loaders should wait for the
afterAuth
hook to be finished before returning.Actual behavior
The
-> app.tsx loader
and-> app._index.tsx loader
are started in parallel. Both loaders start the Authenticating admin request flow. The first loaders calls the-> afterAuth
hook while the secondloader
finishes with<- app._index.tsx loader
before<- afterAuth
and<- app.tsx loader
have finished.Steps to reproduce the problem
Repo: https://github.com/zirkelc/shopify-managed-installation
pnpm shopify app dev