vercel / nextjs-subscription-payments

Clone, deploy, and fully customize a SaaS subscription application with Next.js.
https://subscription-payments.vercel.app/
MIT License
6.23k stars 1.27k forks source link

Include one-time payments option #141

Open andriusmv opened 1 year ago

andriusmv commented 1 year ago

When mapping products, those set as one-time payment (in Stripe) are not rendered. I guess it has something to do with line 107 of the Prixing.tsx component?

{products.map((product) => { const price = product?.prices?.find( (price) => price.interval === billingInterval ); if (!price) return null;

andriusmv commented 1 year ago

Found: needs to add 'one_time' to the SQL file:

https://github.com/vercel/nextjs-subscription-payments/blob/bbb4032829038f86bdb80d349af4dca371a00fb6/schema.sql#L73

andriusmv commented 1 year ago

Please refer to this pull: https://github.com/vercel/nextjs-subscription-payments/pull/142

andriusmv commented 1 year ago

managed to do this by login to supabse, opening the SQL editor, opening a new query and writing this: ALTER TYPE pricing_plan_interval ADD VALUE 'one_time' AFTER 'year'; then hit Run. You should get a success message.

thorwebdev commented 1 year ago

Thanks for documenting your solution. Let's keep this open as a feature request 👍

dalkommatt commented 1 year ago

It looks like the create-checkout-session.ts also needs to be updated for this to work properly. I managed to get it working this way:


import { withApiAuth } from '@supabase/auth-helpers-nextjs';
import { createOrRetrieveCustomer } from 'controllers/supabase-admin';
import { getURL } from 'controllers/helpers';

export default withApiAuth(async function createCheckoutSession(
  req,
  res,
  supabaseServerClient
) {
  if (req.method === 'POST') {
    const { price, quantity = 1, metadata = {} } = req.body;

    try {
      const {
        data: { user }
      } = await supabaseServerClient.auth.getUser();

      const customer = await createOrRetrieveCustomer({
        uuid: user?.id || '',
        email: user?.email || ''
      });

      let session: Stripe.Checkout.Session;
      if (price.type === 'recurring') {
        session = await stripe.checkout.sessions.create({
          payment_method_types: ['card'],
          billing_address_collection: 'required',
          customer,
          line_items: [
            {
              price: price.id,
              quantity
            }
          ],
          mode: 'subscription',
          allow_promotion_codes: true,
          subscription_data: {
            trial_from_plan: true,
            metadata
          },
          success_url: `${getURL()}/account`,
          cancel_url: `${getURL()}/`
        });
      } else if (price.type === 'one_time') {
        session = await stripe.checkout.sessions.create({
          payment_method_types: ['card'],
          billing_address_collection: 'required',
          customer,
          line_items: [
            {
              price: price.id,
              quantity
            }
          ],
          mode: 'payment',
          success_url: `${getURL()}/account`,
          cancel_url: `${getURL()}/`
        });
      }

      return res.status(200).json({ sessionId: session.id });
    } catch (err: any) {
      console.log(err);
      res
        .status(500)
        .json({ error: { statusCode: 500, message: err.message } });
    }
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
});
anup-a commented 7 months ago

@dalkommatt How are you storing one_time payments in tables? Did you create a new Payments table or are you using existing Subscriptions?

If possible, can you share your code snippet. Thanks

dalkommatt commented 7 months ago

@dalkommatt How are you storing one_time payments in tables? Did you create a new Payments table or are you using existing Subscriptions?

If possible, can you share your code snippet. Thanks

I'm using the same subscriptions table. For one-time payments the subscription status becomes 'succeeded' so when I query subscriptions I include that in the query.

const { data, error } = await supabase
    .from("subscriptions")
    .select("*, prices(*, products(*))")
    .in("status", ["trialing", "active", "succeeded"])
    .single()

image

anup-a commented 7 months ago

@dalkommatt Thanks for getting back so quickly.

I don't see any case which handles one_time payments in this repo's webhook.

app/api/webhooks/route.ts

        case 'checkout.session.completed':
          const checkoutSession = event.data.object as Stripe.Checkout.Session;
          if (checkoutSession.mode === 'subscription') {
            const subscriptionId = checkoutSession.subscription;
            await manageSubscriptionStatusChange(
              subscriptionId as string,
              checkoutSession.customer as string,
              true
            );
          }
          break;

I tried writing my own logic, but I was struggling to get price_id, product_id from payment intent. Appreciate your help.

dalkommatt commented 7 months ago

@anup-a this is how I implemented it


        case "checkout.session.completed":
          const checkoutSession = event.data.object as Stripe.Checkout.Session
          if (checkoutSession.mode === "subscription") {
            const subscriptionId = checkoutSession.subscription
            await manageSubscriptionStatusChange(
              subscriptionId as string,
              checkoutSession.customer as string,
              true
            )
          }
          if (checkoutSession.mode === "payment") {
            const paymentIntentId = checkoutSession.payment_intent
            await manageSubscriptionStatusChange(
              paymentIntentId as string,
              checkoutSession.customer as string,
              true
            )
          }
          break
estevanmaito commented 7 months ago

Hey @dalkommatt, did you make any change to manageSubscriptionStatusChange in supabase-admin.ts (mine looks like the on currently in this repo's main)?

Using your code, I get a Stripe error:

StripeInvalidRequestError: No such subscription: 'pi_3Oxxxxxxxxxxxxxxxx'

That's probably coming from this piece in manageSubscriptionStatusChange:

const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
        expand: ["default_payment_method"]
});

Either that or my Stripe product is incorrectly setup. I have one product with 2 prices, one of them being a one time payment like this:

Screenshot 2024-01-30 at 19 37 37
wassimbenr commented 4 months ago

Hey @dalkommatt, did you make any change to manageSubscriptionStatusChange in supabase-admin.ts (mine looks like the on currently in this repo's main)?

Using your code, I get a Stripe error:

StripeInvalidRequestError: No such subscription: 'pi_3Oxxxxxxxxxxxxxxxx'

That's probably coming from this piece in manageSubscriptionStatusChange:

const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
        expand: ["default_payment_method"]
});

Either that or my Stripe product is incorrectly setup. I have one product with 2 prices, one of them being a one time payment like this:

Screenshot 2024-01-30 at 19 37 37

manageSubscriptionStatusChange

Any updates?