1
votes

UPDATED I built a pricing page that uses Stripe Checkout to use both a One-Time payment button for product 1 and a Subscription payment button for product 2.

enter image description here

My goal is to redirect the one time payment button to Stripe Checkout with a one time payment, and separately redirect the subscription payment to a checkout with a recurring payment.

According to STRIPE this can be done using Subscription as the Mode in the CheckoutSession in create-checkout-session.php (sample project) :

The mode of the Checkout Session. Required when using prices or setup mode. Pass subscription if the Checkout Session includes at least one recurring item.

Contrary to the Stripe Docs the following line of code: 'mode' => 'subscription', activates subscription payments ONLY, but it doesnt redirect one time payments. For one-time payments to work I must change it to: 'mode' => 'payment', but then subscription payments don't work.

Here's the php code in question:

 <?php
    
    require_once 'shared.php';
    
    $domain_url = $config['domain'];
    
    // Create new Checkout Session for the order
    // Other optional params include:
    // [billing_address_collection] - to display billing address details on the page
    // [customer] - if you have an existing Stripe Customer ID
    // [payment_intent_data] - lets capture the payment later
    // [customer_email] - lets you prefill the email input in the form
    // For full details see https://stripe.com/docs/api/checkout/sessions/create
    
    // ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
    $checkout_session = \Stripe\Checkout\Session::create([
        'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => $domain_url . '/canceled.html',
        'payment_method_types' => ['card'],
        'mode' => 'subscription',
        'line_items' => [[
          'price' => $body->priceId,
          'quantity' => 1,
      ]]
    ]);
    
    echo json_encode(['sessionId' => $checkout_session['id']]);

And here's the javascript code:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId) {
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId
    })
  }).then(function(result) {
    return result.json();
  });
};

// Handle any errors returned from Checkout
var handleResult = function(result) {
  if (result.error) {
    var displayError = document.getElementById("error-message");
    displayError.textContent = result.error.message;
  }
};

/* Get your Stripe publishable key to initialize Stripe.js */
fetch("./config.php")
  .then(function(result) {
    return result.json();
  })
  .then(function(json) {
    var publishableKey = json.publishableKey;
    var subscriptionPriceId = json.subscriptionPrice;
    var onetimePriceId = json.onetimePrice;

    var stripe = Stripe(publishableKey);
    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("subscription-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(subscriptionPriceId).then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });

    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("onetime-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(onetimePriceId).then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });
      
  });

Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout? How can I accomplish this?

Update according to Bemn:

$checkout_session = \Stripe\Checkout\Session::create([
  'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url' => $domain_url . '/canceled.html',
  'payment_method_types' => ['card'],
  'mode' => $body->mode
    'line_items' => [[
    'price' => $body->price_xxx,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]],

  'line_items' => [[
    'price' => $body->price_zzz,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]]
]);

echo json_encode(['sessionId' => $checkout_session['id']]);

And the JS:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) {
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId,
      mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
    })
  }).then(function(result) {
    return result.json();
  });
};

And the HTML:

<div data-stripe-priceid="pricexxx" data-stripe-mode="payment" id="onetime-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
    
<div data-stripe-priceid="pricexxx" data-stripe-mode="subscription" id="subscription-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
3
Yes, not sure of the documentation but I've seen it many times multiple platforms both options present. :)blanknamefornow
How do you implement that solution? I've tried to solve it on the checkout-sessions.php file. Do you know a better way?ChosenJuan
I remember creating a 'product' from within the Stripe dashboard then in the code creating 'plans' (i.e. payment plans) on the fly depending on the subscription amount. In the backend I used the Stripe API like this:$new_plan = \Stripe\Plan::create([ 'id' => $amount, 'product' => $product_id, 'nickname' => $amount / 100 . ' GBP per month', 'interval' => 'month', 'currency' => 'gbp', 'amount' => $amount, ]); where $product_id is taken from the Stripe dashboard. This was about 18 months ago though so not sure how current it is.wkille
Then I created a new subscription like, $subscription = \Stripe\Subscription::create([ 'customer' => $customer->id, 'items' => [['plan' => $amount]], ]); (nb. I named the plans according to the amount so I could easily check if a plan for a particular amount already existed). Not sure how helpful this is for you, it looks like you're using Stripe in a different way to how I did. I do suspect however that you need to look at 'products' and 'plans'.wkille

3 Answers

1
votes

Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout?

Yes. The key is you should pass the correct options to generate the corresponding Stripe Checkout session ID.

How can I accomplish this?

  • Backend: Have a function to accept Stripe's price ID and payment mode as input and return a Stripe Checkout session ID as the output.

  • Frontend: Pass payment mode information to /create-checkout-session.php. (see the Note if you are unable to do so)


Details

The following solution assuming that:

  1. You generate a Stripe Checkout Session ID at the backend. That ID will then pass to .createCheckoutSession() in js frontend.
  2. You have a 1-time product (let's call it PAY) and a recurrent subscription (let's call it SUB).

Frontend

I think you are close. What you need to do is passing the mode information to your API endpoint as well:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) { // <-- add a mode parameter
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId,
      mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
    })
  }).then(function(result) {
    return result.json();
  });
};

If so, each checkout button in the page should have corresponding info of the priceId and payment mode. You can do so by storing them using data attribute:

<div data-stripe-priceid="price_yyy" data-stripe-mode="subscription">Recurrent</div>
<div data-stripe-priceid="price_zzz" data-stripe-mode="payment">1-time</div>

If so, you can get the data attributes by e.g. a click event.

Note: If you cannot add an extra param to indicate mode, you neeed to identify if the given price ID is a 1-time or recurrent product in the backend. See https://stripe.com/docs/api/prices/object?lang=php#price_object-type for more details.

Backend

Here are 2 sample code snippets from Stripe's documentation. Direct copying of them does not work.

Reference for PAY: https://stripe.com/docs/checkout/integration-builder

$checkout_session = \Stripe\Checkout\Session::create([
  'payment_method_types' => ['card'],
  'line_items' => [[
    'price_data' => [
      'currency' => 'usd',
      'unit_amount' => 2000,
      'product_data' => [
        'name' => 'Stubborn Attachments',
        'images' => ["https://i.imgur.com/EHyR2nP.png"],
      ],
    ],
    'quantity' => 1,
  ]],
  'mode' => 'payment',
  'success_url' => $YOUR_DOMAIN . '/success.html',
  'cancel_url' => $YOUR_DOMAIN . '/cancel.html',
]);

In your case, you may not need to define 'price_data'. Instead, you should use 'price', like the next example.

Reference for SUB: https://stripe.com/docs/billing/subscriptions/checkout#create-session

$checkout_session = \Stripe\Checkout\Session::create([
  'success_url' => 'https://example.com/success.html?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url' => 'https://example.com/canceled.html',
  'payment_method_types' => ['card'],
  'mode' => 'subscription',
  'line_items' => [[
    'price' => $body->priceId,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]],
]);
  1. Take a look at this reference: https://stripe.com/docs/api/checkout/sessions/create. For line_items, you can just simply using 'price' and pass the price ID (e.g. price_xxx), which means your 'line_items' will looks like this:
'line_items' => [[
  'price' => $body->priceId,
  'quantity' => 1,
]],

For 'mode', use the value from your API request. It should be something like:

'mode' => $body->mode

Which means in your backend you better define a function (e.g. generate_checkout_session) to:

  • parse the json body received in the API request
  • get priceId and mode from the parsed data
  • use the priceId and mode in \Stripe\Checkout\Session::create and
  • returns the checkout_session ID

Hope this (and the reference urls) can help you.

0
votes

When you create your Session you can pass both a Price for the recurring amount charged on the subscription and another Price for the one-time fee you want to charge. You can combine multiple recurring Prices and one-time Prices overall.

$checkout_session = \Stripe\Checkout\Session::create([
    'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => $domain_url . '/canceled.html',
    'payment_method_types' => ['card'],
    'mode' => 'subscription',
    'line_items' => [
      // Add a one-time Price for $10
      [
        'price' => 'price_123', 
        'quantity' => 1,
      ],
      // Add another one-time Price for $23
      [
        'price' => 'price_345', 
        'quantity' => 1,
      ],
      // Add a recurring Price for $100 monthly
      [
        'price' => 'price_ABC', 
        'quantity' => 1,
      ],
]);

The code above will create a session with 3 line items. One for $100 monthly, one for $10 just once and one for $23 just once. The total for the session would be $133 on the first payment. It will also start a subscription for $100 a month and future invoices will be for $100 as expected.

0
votes

What I have got is that you just need to add a check for either it is one-time or subscription here is what you can do:

JS FILE CHANGES:

var createCheckoutSession = function(priceId, $mode) {
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId,
      paymentType: $mode, // This vary based on the button clicked either one-time or subscription.
    })
  }).then(function(result) {
    return result.json();
  });
};

/* Get your Stripe publishable key to initialize Stripe.js */
fetch("./config.php")
  .then(function(result) {
    return result.json();
  })
  .then(function(json) {
    var publishableKey = json.publishableKey;
    var subscriptionPriceId = json.subscriptionPrice;
    var onetimePriceId = json.onetimePrice;

    var stripe = Stripe(publishableKey);
    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("subscription-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(subscriptionPriceId, 'subscription').then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });

    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("onetime-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(onetimePriceId, 'onetime').then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });
      
  });

Now we need to make changes in PHP file:

PHP FILE CHANGES:

$checkout_session = \Stripe\Checkout\Session::create([
        'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => $domain_url . '/canceled.html',
        'payment_method_types' => ['card'],
        'mode' => $body->paymentType, // Here is what we have got from front-end
        'line_items' => [[
          'price' => $body->priceId,
          'quantity' => 1,
      ]]
    ]);