Search code examples
asp.net-identityasp.net-core-3.1stripe-payments

asp.net core 3.1 getting current identity user within stripe HttpPost("webhook") returns NULL


I've integrated stripe checkout payments in my website according to stripe's example

Everything works fine. I could verify webhooks are working with stripe CLI and also using ngrok, tunneling my localhost.

Now I've started implementing interaction with the identity database. I wanto to store there the stripe session.CustomerId after the webhook has fired checkout.session.completed.

For that I need to access my Identity database.

My code is:

[HttpPost("webhook")]
    public async Task<IActionResult> Webhook()
    {
        // GET logged current user
        var currentUser = await _userManager.GetUserAsync(User);

        var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
        Event stripeEvent;
        try
        {
            stripeEvent = EventUtility.ConstructEvent(
                json,
                Request.Headers["Stripe-Signature"],
                this.options.Value.WebhookSecret
            );
            Console.WriteLine($"Webhook notification with type: {stripeEvent.Type} found for {stripeEvent.Id}");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Something failed {e}");
            return BadRequest();
        }



        // Handle the event
        if (stripeEvent.Type == Events.PaymentIntentSucceeded) //Success
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            var subscriptionItem = stripeEvent.Data.Object as Stripe.Subscription;
            var subscriptionInvoice = stripeEvent.Data.Object as Invoice;
            var paidTimestamp = paymentIntent.Created;
            var paidValidityTimestamp = subscriptionItem.CurrentPeriodEnd;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent Succeeded");
            // Then define and call a method to handle the successful payment intent.
            // handlePaymentIntentSucceeded(paymentIntent);

            paymentIntent.ReceiptEmail = currentUser.Email;
            paymentIntent.Shipping.Address.City = currentUser.City;
            paymentIntent.Shipping.Address.PostalCode = currentUser.PostalCode.ToString();
            paymentIntent.Shipping.Address.Line1 = currentUser.Street + " " + currentUser.CivicNumber;

            currentUser.hasPaidQuote = true;
            currentUser.paidOnDate = paidTimestamp;
            currentUser.paidValidity = paidValidityTimestamp;
            currentUser.SubscriptionStatus = paymentIntent.Status;
            currentUser.Status = LoginUserStatus.Approved;
            await _userManager.UpdateAsync(currentUser);

            Console.WriteLine("has paid? " + currentUser.hasPaidQuote);
            Console.WriteLine("paid on date: " + currentUser.paidOnDate.Date.ToString());
            Console.WriteLine("payment validity: " + currentUser.paidValidity.Date.ToString());
            Console.WriteLine("subscription status: " + currentUser.SubscriptionStatus);
            Console.WriteLine("user status: " + currentUser.Status.ToString());
            Console.WriteLine("customer ID: " + paymentIntent.Customer.Id);
            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status from paymentIntent: " + paymentIntent.Invoice.Status);
            Console.WriteLine("invoice status from subscriptionItem: " + subscriptionItem.LatestInvoice.Status);
            Console.WriteLine("subscription status: " + subscriptionItem.Status);
            Console.WriteLine("invoice status: " + subscriptionInvoice.Paid.ToString());

            Console.WriteLine(".....................");
            Console.WriteLine("*********************");

        }
        else if (stripeEvent.Type == Events.PaymentIntentPaymentFailed) //Fails due to card error
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent requires payment method. Payment failed due to card error");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);

            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status: " + paymentIntent.Invoice.Status);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }
        else if (stripeEvent.Type == Events.PaymentIntentRequiresAction) //Fails due to authentication
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent requires actions");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);

            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status: " + paymentIntent.Invoice.Status);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }
        else if (stripeEvent.Type == Events.PaymentMethodAttached)
        {
            var paymentMethod = stripeEvent.Data.Object as PaymentMethod;
            Console.WriteLine("Payment Method Attached");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);
        }
        // ... handle other event types




        if (stripeEvent.Type == "checkout.session.completed")
        {
            var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
            ViewBag.eventID = stripeEvent.Id;

            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("checkout session completed");
            Console.WriteLine($"Session ID: {session.Id}");

            // Take some action based on session.
            currentUser.StripeCustomerId = session.CustomerId;
            await _userManager.UpdateAsync(currentUser);

            Console.WriteLine("Ssession.CustomerId has been retrieved from session and will be stored into database: " + session.CustomerId);
            Console.WriteLine("StripeCustomerId has been stored into database: " + currentUser.StripeCustomerId);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }

        return Ok();
    }

My problem is that var currentUser = await _userManager.GetUserAsync(User); is returning NULL.

If I run the code in debugging mode a blue circle with an exclamation mark is showing up on my braking point. Hovering the mouse There I get the notification: "The process or thread has changes since last step". Using checkout API of stripe redirects me somewhere, losing the information of my session, hence losing also any link to my Identity database.

How can I obtain that my variable currentUser contains the logged-in user?

My PaymentsController is as follows:

public class PaymentsController : Controller
{
    public readonly IOptions<StripeOptions> options;
    private readonly IStripeClient client;
    private UserManager<VivaceApplicationUser> _userManager;

    private readonly IConfiguration Configuration;


    public IActionResult Index()
    {
        return View();
    }

    public IActionResult canceled()
    {
        return View();
    }

    public IActionResult success()
    {
        return View();
    }

    public PaymentsController(IOptions<StripeOptions> options,
        IConfiguration configuration,
        UserManager<VivaceApplicationUser> userManager)
    {
        this.options = options;
        this.client = new StripeClient(this.options.Value.SecretKey);
        _userManager = userManager;
        Configuration = configuration;
    } .......

}

My [HttpGet("checkout-session")] is

[HttpGet("checkout-session")]
    public async Task<IActionResult> CheckoutSession(string sessionId)
    {
        var currentUser = await _userManager.GetUserAsync(HttpContext.User);
        var service = new SessionService(this.client);
        var session = await service.GetAsync(sessionId);

        // Retrieve prices of product
        var product = new PriceService();
        var productBasic = product.Get(Configuration.GetSection("Stripe")["BASIC_PRICE_ID"]);
        var productMedium = product.Get(Configuration.GetSection("Stripe")["MEDIUM_PRICE_ID"]);
        var productPremium = product.Get(Configuration.GetSection("Stripe")["PREMIUM_PRICE_ID"]);

        int quoteBasic = (int)productBasic.UnitAmount;
        int quoteMedium = (int)productMedium.UnitAmount;
        int quotePremium = (int)productPremium.UnitAmount;

        int selectedPlan = (int)session.AmountTotal;
       
        if (!string.IsNullOrEmpty(session.Id))
        {
            // storing stripe customerId into the User database
            // this will be done if checkout.session.completed
            //currentUser.StripeCustomerId = session.CustomerId;
            
            if (selectedPlan == quoteBasic)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Basic;
            }
            else if (selectedPlan == quoteMedium)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Medium;
            }
            else if (selectedPlan == quotePremium)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Premium;
            }

            await _userManager.UpdateAsync(currentUser);
        }

        return Ok(session);
    }

In the "checkout-session" I can retrieve the currentUser without any problem using simply:

var currentUser = await _userManager.GetUserAsync(HttpContext.User);

How canI get it up and running in the webhook method?


Solution

  • Thank to CJAV I was able to find a way to get it up and running. I've also found very valuable information here Unable to retrieve a session's variable values in a Stripe webhook and also here Reconciling "New Checkout" Session ID with Webhook Event data.

    In my [HttpPost("create-checkout-session")] I've added a reference to ClientReferenceId = currentUser.Id as follows:

    var options = new SessionCreateOptions
    {
        .....
    
        PaymentMethodTypes = new List<string>
        {
            "card",
        },
        Mode = "subscription",
        LineItems = new List<SessionLineItemOptions>
        {
            new SessionLineItemOptions
            {
                Price = req.PriceId,
                Quantity = 1,
            },
    
        },
        Metadata = new Dictionary<string, string>
        {
            { "UserId", currentUser.Id },
        },
        ClientReferenceId = currentUser.Id
    };
    

    My [HttpPost("webhook")] is now:

        .....
    // Handle the event
    if (stripeEvent.Type == Events.PaymentIntentSucceeded) //Success
    {
        var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
        var customerId = paymentIntent.CustomerId;
        var invoiceId = paymentIntent.InvoiceId;
        var fetchedEmail = paymentIntent.ReceiptEmail;
    
        var customerService = new CustomerService();
        var fetchedCustomer = customerService.Get(customerId);
    
        var invoiceService = new InvoiceService();
        var fetchedInvoice = invoiceService.Get(invoiceId);
    
        var currentUser = await _userManager.FindByEmailAsync(fetchedEmail);
    
        //var customerReferenceFromSession = session.Metadata.GetValueOrDefault("UserId");
        //var currentUser = await _userManager.FindByIdAsync(customerReferenceFromSession);
    
        var paidTimestamp = paymentIntent.Created;
        Console.WriteLine("*********************");
        Console.WriteLine(".....................");
        Console.WriteLine("Payment Intent Succeeded");
        // Then define and call a method to handle the successful payment intent.
        // handlePaymentIntentSucceeded(paymentIntent);
    
        var Line1 = currentUser.Street + " " + currentUser.CivicNumber;
    
        var paymentIntentOptions = new PaymentIntentUpdateOptions
        {
            Shipping = new ChargeShippingOptions()
            {
                Address = new AddressOptions
                {
                    City = currentUser.City,
                    PostalCode = currentUser.PostalCode.ToString(),
                    Line1 = Line1
                },
                Name = currentUser.LastName + " " + currentUser.FirstName
            },                    
        };
        var paymentIntentService = new PaymentIntentService();
        paymentIntentService.Update(
        paymentIntent.Id,
        paymentIntentOptions
        );
    
        paymentIntent.ReceiptEmail = currentUser.Email;
    
        currentUser.hasPaidQuote = true;
        currentUser.paidOnDate = paidTimestamp;
        currentUser.SubscriptionStatus = paymentIntent.Status;
        currentUser.Status = LoginUserStatus.Approved;
        await _userManager.UpdateAsync(currentUser);
    
        Console.WriteLine("has paid? " + currentUser.hasPaidQuote);
        Console.WriteLine("paid on date: " + currentUser.paidOnDate.Date.ToString());
        Console.WriteLine("subscription status: " + currentUser.SubscriptionStatus);
        Console.WriteLine("user status: " + currentUser.Status.ToString());
        Console.WriteLine("customer ID: " + paymentIntent.CustomerId);
        Console.WriteLine("payment intent status: " + paymentIntent.Status);
    
        Console.WriteLine("invoice status from subscriptionItem: " + subscriptionItem.LatestInvoice.Status);
        Console.WriteLine("subscription status: " + subscriptionItem.Status);
        Console.WriteLine("invoice status: " + subscriptionInvoice.Paid.ToString());
    
        Console.WriteLine(".....................");
        Console.WriteLine("*********************");
    
    }
    

    And it works!! It would be great to make a short notice in the stripe docs.

    Have fun in coding!