Open CuttyBang opened 7 years ago
Hi Nathan,
I'm glad you find this useful!
What's your application URL set in Shopify admin? It's probably '/'.
When you access '/' in development, the webpack dev server bypasses the server side routing and loads index.html and the app. It works alright in production. Use ngrok url/home instead of ngrok url/. Also, don't forget to create an .env file in react-ui folder. Those values are used to have the embedded app play nicely with the dev server.
This restriction with not being able to use '/' as your app route is something we need to fix but I didn't put any thought into it yet. Suggestions are welcome.
Hey Mihovil,
Thank you for responding so quickly. You're awesome!
Man, I WISH it were that simple.
I definitely have the .env file in react-ui. Perhaps I'm not satisfying it's requirements? Mine looks like this:
REACT_APP_SHOPIFY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyz REACT_APP_SHOP_ORIGIN=devstore.myshopify.com
And I definitely have the app URLs set as:
https://12345.ngok.io/home <--App URL https://12345.ngok.io/auth/callback <--Whitelist
That's why it's so vexing! Everything was totally fine at first and I was able to install perfectly - I had the product grid and everything- then I went to bed........
The next day, and every day since, I get that demon-speak in my terminal upon every install. >: ^ (
Without knowing what could be causing the issue, I've even washed and sanitized the whole thing and started over from scratch. Yet it still errors. I guess I was hoping you'd have some sort of magic bullet.
Could it be the specific ngrok port?
Hi Nathan,
The root problem is that a request is not hitting any of the routes and then hitting the generic route which tries to load the built index.html. When you look at the ngrok logs, what is the request that triggers this error? I think this will give us the answer. It's probably something trivial.
Could you paste the contents of your .env
file in the root folder?
The offending route is/was '/api/products', and instead of showing the products, it gave that error (while the little progress bar slooooooowly creeps across the app pane).
Funny thing is, I managed to get it working again. I'm still not entirely sure what fixed it, but I took a number of precautions that have solved many dev issues I've had with Shopify apps in the past. Those being: -Deleting the app in the dev store (obviously). -Deleting the app altogether in the admin and creating an entirely new application with new creds. -Clearing the cache in Chrome and all cookies associated with the installation store (probably the biggest issue creator for Shopify app development I've seen. Chrome is too 'helpful' for it's own good sometimes, and Shopify too rigid). -Updated package.json where applicable/appropriate. Then voila, things started going my way again.
Unfortunately, I'm facing a different issue now.
Ah, but such is the way of working with Shopify.
Just in case - must I do anything in particular (declare or register routes somewhere, use a make calls via a specific route, etc) in order to make requests out of this? It seems like very little of my express/node knowledge is serving me very well so far in this endeavor, so I just wanted to check if there's an easter egg I failed to notice yet. I don't seem to be able to make http requests for some reason.
(full disclosure - I'm not the worlds biggest React fan so I don't use create-react-app as often as some people. Every time I use it for projects I feel like there's some little peculiarity I forget to take into account)
Thank again my man!
How are you making the requests? What error are you getting? Do you have a repository I can check out :)?
When you install the app into a store, you get a session with the Express server. All requests towards Shopify need to go through the server. You can't make requests to Shopify admin from the client (React) side. Every request needs to be proxied through the Express server. This is a wild guess, but are you trying to call the Shopify API directly from the React app?
You should be able to make any request you have authorizations for through shopify-api-node.
Hey, sorry for responding so late, I set up a webhook, then make a post request in response to the webhook. It's just a test I use to check for webhook communications. The webhook is working just fine, but my post requests are not.
I've tried to make them in server.js, then in app.js, then in a separate file and they just don't seem to be listening. I tried different endpoints as well, in every location I made to request, and still no action. The endpoints I've tried to use are:
router.post('/new-orders', (req, res) => {
res.status(200);
}):
router.post('/api/new-orders', (req, res) => {
res.status(200);
}):
and those same endpoints from
app.post(...)
in all different files and to no avail. Frankly, it's been driving me insane since I've never had these issues before. That's why I wanted to double check if there was some sort of procedure in place.
:^/
here, check it out...
Hi Nathan,
I can't see the repository. Is it private? Could you paste the part of the code in your webhook where you're making the request?
Are you trying to make a request to the Shopify API in the webhook or to the Express server?
I have an idea about the problem. If it's that, the solution is easy. If you want to make a request to the Shopify API in a webhook, you don't have an authenticated session and you need a token. The solution would be simply to store the token in the Shops table when the shop completes the oAuth flow. Then, in the webhook, find the shop by its name and use its token to call the Shopify API.
I hope this helps. If not, a code example will help us easily find a solution.
Aww, crap. Sorry. Yea, it's a private repo. Here, I'll just copy it in....
So I create the webhook with the other webhook you have set up to create...
const afterShopifyAuth = session => {
const shopify = getShopifyApi(session);
const webhook = {
topic: 'app/uninstalled',
address: `${APP_URL}${UNINSTALL_ROUTE}`,
format: 'json'
};
const newOrders = {
topic: 'orders/create',
address: `${APP_URL}${NEW_ORDERS}`,
format: 'json'
};
shopify.webhook.create(newOrders);
shopify.webhook.create(webhook);
};
The 'NEW_ORDERS' path is set in config:
const NEW_ORDERS = '/new-orders';
//have also tried it at '/api/new-orders' since you have other requests set up at that endpoint.
Then as I said before, I'm making the requests all over the place in an attempt to figure out where it wants to find them. I've tried in app.js, shopify.js, and in a separate file (.orders.js). They're basically all the same request, except I've tried to make them in as many ways I can think of. At this point, all I want it to do is return a 200 status, but the request isn't registering the ping from the webhook at all. The requests look like so:
//also tried as 'router.post(...)' & using the '/api/new-orders' endpoint
app.post('/new-orders', (req, res) => {
res.status(200);
});
Again, this has been attempted from app.js, shopify.js, and from a separate file.
I've successfully made this same request a hundred times in my other apps, so I'm a little baffled here. I would really like to get this ironed out because you've got a beautifully constructed template that's an amazing time saver.
Oh, and I realize that I said I was making requests in server.js before. I'm sorry. I meant shopify.js. The webhook is working fine. It's the responding to the ping from the webhook that's the issue.
When you say that the webhook is working fine, you mean creating the webhook or that Shopify hits your webhook when you create an order?
What response are you getting?
Try putting the new-orders endpoint before the auth middleware in shopify.js
. I'm not sure if we're talking about the same problem, but I just tried hitting the new-orders endpoint and got a 200 response.
That the webhook is successfully created and I get a touchback when an order is made. All I end up getting is a 302. And for some bizarre reason, I get a 304 somewhere in the mix. I'm not real worried about that one though, since it looks like it's coming from the app pane in admin.
I'll drop it in up there before the the auth middleware as you say. As of now -when I'm calling it inside shopify.js
- I've got it way down at the bottom with the '/logout'
request. Or wherever you had the '/orders'
placeholder. I thought it might've been a breadcrumb you left to illustrate where you intended those to go.
I'll report back with my findings.
Thanks again for shepherding me through this. If you're getting it to behave, then clearly I'm not doing it right.
Hi Nathan,
If you put your route after the auth middleware, it'll need an authenticated session. If you put it before the auth middleware, it should work. Let me know if this works ;).
I dunno, what line were you putting yours? I gotta get this to work correctly once before I jump off a bridge...
I put it in at around line 238, just before the authMiddleware()
function, and just after the uninstall post request.
The webhook is coming through, but it's not taking the status code. I'm simply doing that simple response that I showed you before:
router.post('/new-orders', (req, res) => { res.status(200) });
but it's not landing. I get a blank (in the ngrok logs), one of these numbers in the winston logs
POST /new-orders - - ms - -
then it gives me one of these...
Proxy error: Could not proxy request /new-orders from 0f7486c3.ngrok.io to http://localhost:3001.
See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (ECONNRESET).
[HPM] Error occurred while trying to proxy request /new-orders from 0f7486c3.ngrok.io to http://localhost:3001 (ECONNRESET) (https://nodejs.org/api/errors.html#errors_common_system_errors)
Ok, fine. Maybe I just need to feed it an actual job to do. Seems reasonable. All I want is to complete a successful handshake. SO CLOSE.
So I trimmed it out like so:
router.post('/new-orders', (req, res) => {
const { shopify } = req; // how you're declaring it both before and after this.
const orderData = req.body;
const orderId = orderData.id;
shopify.transaction.create(orderId, {kind:"capture"}).then(
transaction => { res.status(200).json(transaction); },
error => { console.log(error); });
});
Now, when I do this, things really go crackers. Suddenly the logs are screaming this at me:
POST /new-orders 500 94.876 ms - 3790
error: TypeError: Cannot read property 'transaction' of undefined
at /Users/nathanmcelwain/Sites/localhost/blap/compiled/webpack:/server/routes/shopify.js:237:5
at Layer.handle [as handle_request] (/Users/nathanmcelwain/Sites/localhost/blap/node_modules/express/lib/router/layer.js:95:5)
at next (/Users/nathanmcelwain/Sites/localhost/blap/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/Users/nathanmcelwain/Sites/localhost/blap/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/Users/nathanmcelwain/Sites/localhost/blap/node_modules/express/lib/router/layer.js:95:5)
at [ ... ]
It goes on, but you get the gist.
You've got successful declarations like that all over the place -before and after- so I'm kinda bummed it's not into it with me. That's why I'm wondering what line you're making requests on, so (for the sake of my sanity) I can just copy you. Instead of trying to write an second getShopifyApi()
for myself, I'd really like to just use what clearly already works.
I'm sorry man, I've never had so much trouble getting an already working application to work...
Hi Nathan,
Try this :):
router.post('/new-orders', (req, res) => {
res.status(200).send('Ok');
});
You're missing the send() (or json()).
Btw, the 500 error is expected. When you authenticate with Shopify with the client app, the token is stored in the session (backed by express-session and Redis). This is how we know that an authenticated shop is making requests to its admin.
But, in a webhook, it's Shopify that's making the request. There is no session and no shopify object attached. But again, we know it's a request from Shopify because we validated the HMAC. At that point, you need to find the shop in your database and find its token. Then you can instantiate the shopify-api-node object and make a request. Of course, you need to save the token to the database table after authentication. Let me know if you need more help on this.
Oh, man. Is THAT what's going on? So weird. Sometimes you get so focused on a tiny little detail that the bigger picture disappears.
This is gold. Thank you man. I'll try not to bother you again.... :^D
Well, maybe not quite yet....
Do you have any db lookup methods already in place?
Just to double check, the webhook works?
For the database, save the token on the auth callback route. Find the line with the Shop.findOrCreate call. You'll have to add the token field to Shop model and migration. It's straightforward. Then, in your webhook use Shop.find with a where clause that finds by domain. An example is in the uninstall route, although it uses destroy instead of find.
I'm happy you find this starter valuable so feel free to ask questions. It'll help other people that find this. Are you building a production Shopify app, private or public?
Oh cool! This keeps getting better and better. Thank you sir!
Yea man, this starter is a gem. I'm really stoked to have found it.
This particular app that I'm using the starter for is going to be a "private public app", or an unlisted app for a client, but I build different types. Some are just convenience apps, but I usually need to solve some sort of problem or special need and private app functionality is limited.
We've done custom checkouts, loyalty program type apps, buy one/get one, etc. Our clients are almost exclusively tech companies, and we end up building a lot of purchasing portal/internal/bulk ordering stores, as well as their public facing brand merchandise storefronts, so special use cases are the norm. A lot of them would probably be useful as a truly "public" app, albeit somewhat specialized.
Oh, and yes, the webhook works exactly as expected. :^D
Ok, so we want to do something like this?
//on AUTH_CALLBACK_ROUTE
[...]
return Shop.findOrCreate({
where: {
domain: shop,
token: token
}
})spread(() => { [...]
where 'shop' & 'token' are defined above as...
[...]
return shopifyToken
.getAccessToken(shop, code)
.then(token => {
session.shopify = { shop, token };
[...]
Then, in db/models/shop.js
we want to add 'token' to the Shop model like so?
[...]
},
token: {
type: DataTypes.STRING,
unique: true
},
chargeId: {
[...]
and in migrations...
[...]
},
token: {
type: Sequelize.STRING,
unique: true,
},
chargeId: {
[...]
then we can instantiate the shopifyApi
Shop.find({
where: {
domain: req.body.shop
}
}).then((shop) => {
const thisShop = new ShopifyApi({
shopName: shop.domain.split('.')[0],
accessToken: shop.token
});
});
Half clarifying, half creating a user guide.
Are we saving just the shopify domain in the db? ('this-shop-name.myshopify.com') Or the full url?
Specifically for 'Shop.find' promise.
Plus, you'd have to RE sequelize the db by running npm run sequelize db:migrate
again after altering the model, correct?
Hi Nathan,
Yeah, that sounds right. Have you tried it out? We're just saving the domain. If you already have existing data in the database, you'll need to create a migration. Otherwise, I'd just wipe out existing data and start with the new model.
I did but I might have done something wrong. Still back tracking
What I decided to do was start over fresh from the beginning so I could establish a set up.
So everything was fine until I got to npm run sequelize db:migrate
when it started to tell me that
ERROR: DataTypes is not defined
I didn't have that issue before. And as far as I know, DataTypes is just a convenience class, but you can access it directly from the Sequelize object anyway. But I tried importing it directly just the same and that didn't really help.
I peaked into the db table and can see the sequelize meta in there, so I thought maybe the error was erroneous.
I can compile the code, but installing throws an even lamer error that says:
SequelizeDatabaseError: relation "Shops" does not exist
and this is being born from the .findOrCreate()
method.
This is my nightmare. I'll let you know if I iron it out...
The second error is probably due to not running the migration. The first one makes no sense. Can you compare Sequelize versions between your previous setup, which worked, and the new one?
I worked it out. Something got deleted I think. It's all looking good AFAICT.
What, in your opinion, is the most reliable place to query the db? There isn't anything that comes with the webhook that includes the domain, so I'm calling a function that queries the db inside checkForValidSession()
since it carries it in the req object. But that's decidedly not reliable for production.
Shopify sends you the domain with the webhook. It's in the body:
Check line 207 in shopify.js
. Is this what you need?
I thought so too, but when I look at the req object, I don't see anything that mentions the domain explicitly. Which is strange, since you'd think it would have to carry the domain.... [shrug] That line at 207 is pretty much the same as mine. I'm just using it somewhere else. I'll try to pull it out of the webhook again. Thanks again my man.
Hi @CuttyBang,
I totally forgot that this thread is still open. You're right. Shopify sends the domain in one of the headers and not the body. I'm not sure if that's a recent change or I misread the documentation initially. I fixed this and pushed the change to master. Mystery solved :).
All good my man. I found it in the header too. But, see what I mean? I told you so :^D HA!
I actually figured out how to pull the shop name out of the webhook object, although it's a tad hack-ish since webhook actually does include the shop name, albeit not in the way you would think - or want - but it does. There's a link to the order status page in the JSON object. I used that, split it up by the forward slash, and targeted the shop name that way. Like I said, hack-ish, and not ideal. But in lieu of getting into the header and writing up a bunch more code, I made that work. I meant to write up a "real" solution personally, but sounds like you beat me to the punch! That's awesome. I'm going to test it out. We should write up a little user guide for this. I think it would help people. I've banged around with this enough at this point to know where the stumbling blocks can pop up. Especiall for people who are unfamiliar with Sequelize, or Redis, or even the create-react-app. You mind if I spin a little guide?
Just pull the new code and you'll see how the access the header. The uninstall webhook never worked properly and I have no idea how I missed it. I think I read somewhere that Shopify sends you the domain in the body. But, that was either some legacy documentation or I've misread it. It seems to be causing problems for people, as seen in the other issue thread.
Spinning up a guide is a great idea. It would help a lot of people and I'd really appreciate it!
This looks great. However, I'm suddenly getting a sequelize error. The old "Cannot find ../config.json. Have you run sequelize init?" error. There's been literally zero change on my end as far as workflow/set up. Is it possible that something got moved or deleted by accident? I'll try to perhaps update the versioning in package.json, as that seems to be one of sequelize's big complaints as is, however, I think that will make it so I have to start using the symbols thing......
Let me know if you have any ideas.
Something probably got deleted. Did you make any changes at all? You don't need to update sequelize-cli if it worked before. Does this file exist? Where does the error originate?
Sorry this took me so long.
Actually, no. I started from scratch with the new changes and had the issue. So I scrapped it and tried again, same issue. I did it 3 times before even bringing it up. The old version seems to work just fine, albeit without the correct headers.
Thank you for setting one of these up. It's an incredible timesaver.
I had a prefect install with this initially, just a flawless victory from start to finish, but now when I attempt to install I get the old: " ENOENT: no such file or directory, open '/react-ui/build/index.html' " every time - even when I scrap and create a fresh everything (including test store, creds, db, etc).
The main issue, as I see it, is the fact that it's looking for that file, in that directory, in the first place. Honestly, there really IS no such file, because the dir doesn't even exist (at this point), it's just on localhost, and not even compiled yet. So now I'm scouring the whole thing looking for the culprit and I can't really track down what would even cause the error (aside from looking for a file in a directory that doesn't exist).
Any thoughts on this? I can't quite reconcile why it went so smoothly initially, yet now it just refuses to act right. Any insight would be gold.
Thanks