nozzlegear / ShopifySharp

ShopifySharp is a .NET library that helps developers easily authenticate with and manage Shopify stores.
https://nozzlegear.com/shopify-development-handbook
MIT License
743 stars 309 forks source link

How to use Webhook using this Libraray? #917

Open Chirag0406 opened 1 year ago

Chirag0406 commented 1 year ago

Hi Team,

I want to understand how we can retrieve webhooks in Dotnet 6.0 version.

I want to update my SQL tables in two conditions:

  1. When a new Product is created
  2. When an existing Product is updated

I tried understanding the document but could not find a proper example. Can someone please help me here

nozzlegear commented 1 year ago

Hey @Chirag0406! Can you clarify what you mean by retrieving webhooks? Do you mean you want to create a webhooks for those two conditions you listed and use them to update your SQL tables when those events occur?

In that case, it's important to know that you'll need some kind of web server or application at a publicly reachable URL (not localhost) that can process HTTP requests from Shopify. When you create a webhook using the Shopify API, you're essentially telling Shopify "when this event occurs [a product is created or updated], ping the URL I'm giving you to notify me".

For this example I'll just assume that you're using an ASP.NET 6 app that's processing the webhooks requests directly. [1] First you need to use the API to create the webhooks, but beware that you can only create a webhook once per topic/address/store combination; an exception will be thrown if you try to create one that's not unique.

public async Task CreateWebhooksForUserAsync(MyUserModel user)
{
  IWebhookService service = new WebhookService(user.MyShopifyDomain, user.AccessToken);

  // TODO: make sure this user doesn't already have the webhooks you want to create
  // You could do this by listing them using service.ListAsync, or you could store the ids of the webhooks you create in your database and check if the user has those ids already

  // Create the new webhooks
  var productCreatedWebhook = await service.CreateAsync(new Webhook
  {
    Topic = "products/create",
    Address = "https://my-domain.com/webhooks/products/created"
  });
  var productUpdatedWebhook = await service.CreateAsync(new Webhook
  {
    Topic = "products/update",
    Address = "https://my-domain.com/webhooks/products/updated"
  });

  // TODO: maybe store the ids of the webhooks you just created in your database, and use them as a check to see if the user has already created the webhooks.
  // They can also be used when updating the webhooks, in case you need to change the address in the future.
}

Then you can set up a controller with actions to handle each webhook you created. Shopify will send HTTP POST requests to these webhooks whenever a product is created or updated on your users' shops. Each request will include an X-Shopify-Shop-Domain header, which you can use to determine which shop the current request is referring to.

The webhook should be validated using Shopify's validation scheme, which ShopifySharp can help you with using the AuthorizationService.IsAuthenticWebhookAsync method. Doing this will ensure nobody discovers your webhook endpoints and tampers with them or sends you fake data, but it does require rewinding the request stream or else you won't be able to deserialize the product data from the webhook body.

So with all that in mind, here's what your webhook body might look like:

[Route("webhooks")]
public class WebhooksController : ControllerBase
{
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var request = context.HttpContext.Request;

        // Enable buffering, which lets the request stream be read multiple times
        request.EnableBuffering();

        // Validate the request using ShopifySharp
        var isAuthentic = await AuthorizationService.IsAuthenticWebhook(request.Headers, request.Body, shopifyAppSecretKey);

        if (request.Body.CanSeek)
        {
          request.Body.Position = 0;
        }

        if (!isAuthentic)
        {
            context.Result = Unauthorized("Request does not pass Shopify's validation scheme.");
            return;
        }

        await base.OnActionExecutionAsync(context, next);
    }

  [HttpPost, Route("products/created")]
  public async Task<IActionResult> ProductCreated([FromHeader(Name = "X-Shopify-Shop-Domain")] string shopDomain, [FromBody] Product product)
  {
    var user = await database.GetUserByShopDomain(shopDomain);

    // TODO: do something with the new product here

    return Ok()
  }

  [HttpPost, Route("products/updated")]
  public async Task<IActionResult> ProductUpdated([FromHeader(Name="X-Shopify-Shop-Domain")] string shopDomain, [FromBody] Product product)
  {
    var user = await database.GetUserByShopDomain(shopDomain);

    // TODO: do something with the updated product here

    return Ok()
  }
}

I haven't tested this code but this is a broad overview of what you need to do. This is how I do it in my own Shopify applications, with one exception: the controller in this example code is using an override for OnActionExecutionAsync to validate each webhook request and return a 401 Unauthorized result if the request doesn't pass. In my own apps, I move this to an authorization attribute instead of using an override. But they accomplish the same thing.

Let me know if this works for you!

[1] If you want to get fancy you could set up something like a microservice that just receives the webhook pings and stores them in a database to be processed later by a daemon or other service.

Chirag0406 commented 1 year ago

Thanks for the quick response!

I create a webhook in notification for Product Creation on Shopify Store image

This is the C# code I wrote to update my SQL Tables with New record:

[HttpPost, Route("/NewProductCreated")]
        public async Task<IActionResult> ProductCreated([FromHeader(Name = "X-Shopify-Shop-Domain")] string shopDomain, [FromBody] Product product)
        {
            //var user = await database.GetUserByShopDomain(shopDomain);
            // TODO: do something with the new product here

            var carronProductViewModel = new CarronProductViewModel();
            carronProductViewModel.PdShopifyId = product.Id;
            carronProductViewModel.PdTitle = product.Title;
            carronProductViewModel.PdDesc = product.BodyHtml;
            carronProductViewModel.PdTags = product.Tags;
            carronProductViewModel.PdHandle = product.Handle;
            carronProductViewModel.PdProdType = product.ProductType;
            carronProductViewModel.PdVendor = product.Vendor;
            carronProductViewModel.PdStatus = product.Status;
            carronProductViewModel.PdChildSkuLst = String.Join(",", product.Variants.Select(x => x.SKU).Distinct().ToList());
            carronProductViewModel.PdOpt1Lst = String.Join(",", product.Variants.Select(x => x.Option1).Distinct().ToList());
            carronProductViewModel.PdOpt2Lst = String.Join(",", product.Variants.Select(x => x.Option2).Distinct().ToList());
            carronProductViewModel.PdOpt3Lst = String.Join(",", product.Variants.Select(x => x.Option3).Distinct().ToList());

            var resultProduct = await _carronRepository.InsertProductData(carronProductViewModel);

            return Ok();
        }

But the controller doesnt recieve any values and Product object is empty. Apart from this I created same flow for Update Product too, that shown null value for Project as well.

nozzlegear commented 1 year ago

Okay, this looks mostly correct. Did you also implement the AuthService.IsAuthenticWebhook check? If so, I think I forgot to rewind the request stream, which would cause the product to be null there. The override method should look like this:

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var request = context.HttpContext.Request;

        // Enable buffering, which lets the request stream be read multiple times
        request.EnableBuffering();

        // Validate the request using ShopifySharp
        var isAuthentic = await AuthorizationService.IsAuthenticWebhook(request.Headers, request.Body, shopifyAppSecretKey);

      if (request.Body.CanSeek)
      {
        request.Body.Position = 0;
      }

        if (!isAuthentic)
        {
            context.Result = Unauthorized("Request does not pass Shopify's validation scheme.");
            return;
        }

        await base.OnActionExecutionAsync(context, next);
    }