DEV Community

G. S. Perilli
G. S. Perilli

Posted on

Stripe Elements in Rails and Payments Without Email Submissions

One of my freelance clients asked for a payment feature that did not require registering an email a year ago, and so I set about coding this using Stripe Elements. If you are building an ecommerce web site, often you might set up an admin login page, where sales, and inventory can be monitored etc., and an authenticated user page where a user can monitor their purchases. The main difference in this case was that the users of the service could put money into the website app, and monitor this amount from their own account, but they would never need to disclose their email address, even at the payment stage. They would never get an emailed receipt in this case aswell.

My personal version of the app is here: https://github.com/gperilli/fundme

This is a ready-made “fundme” web app, where users/fans can sign up, login and pay money into a project or artist or something. The amount they pay in can be seen on their post-login page.

Stripe Payments

Integrating Stripe payments is fairly simple if using the older service from Stripe. Essentially you create a Stripe account, get the account keys, and then set up something to be sold on your site. At the checkout stage the user will be taken to a page hosted by Stripe where the transaction is completed (with a non-customizable email submission).

The main advantage of doing it this way is, that you can spend your time developing your e-commerce site, and you never deal with credit card number submissions, and storage on your database. However if you need more options, and you want to embed the Stripe checkout into your site, then you need Stripe Elements.

Stripe Elements

A big advantage of Stripe Elements is that the checkout can be put in a modal or something. The user can select the thing they want to buy, fill out their details, and complete the payment without leaving a single page.

For Stripe’s documentation on this, take a look at here:https://stripe.com/docs/payments/elements

The strpe elements checkout is essentially an embeddable iframe with more options than the older Stripe service.

Using Stripe Elements Within a Rails Framework

My main source for setting up Stripe elements in a Rails framework was this: https://github.com/cjavilla-stripe/rails-accept-a-payment. The accompanying Youtube video is here: https://www.youtube.com/watch?v=VY9IwMsMSMY&t=2194s. From that tutorial and sample code, I developed a subscription payment system using Redis to schedule events on the server side using Rails’ background jobs. For more information on background jobs, checkout the official documentation.

Before explaining my approach to developing a monthly / yearly subscription feature, we need to actually test out fake payments using Stripe Elements. The basic flow for achieving this is as follows:

  1. The Stripe publishable key (which you get after creating a Stripe account) is used client side to generate an instance of Stripe
  2. When the payment form submit button is clicked, the form's submission is delayed using preventDefault(), and a POST request is made using Javascript’s fetch to trigger the generation of a Stripe "payment intent".
  3. Over on the server side the Stripe payment intent is generated which would require the ammount to be paid etc., and then returned to the client side payment form.
  4. The Stripe instance then deals with the payment using the paymentIntent.client_secret returned from the POST request (credit card handling by Stripe etc.)
  5. And, finally the payment form is submitted with the payment intent id pasted into a hidden input in the payment form. In my web app this completes the process by generating a new instance of a Donation model, and recording a payment method id from Stripe which can be used to issue future payments.

The Javascript handling the Stripe Elements iframe and the Javascript fetch method looks like this in my project:

<script charset="utf-8">

  var stripe = Stripe('<%= Rails.configuration.stripe[:publishable_key] %>');
  // load the fonts in
  var fonts = [{
    cssSrc: "https://fonts.googleapis.com/css?family=Karla",
  }];
  // styles for the stripe inputs
  var styles = {
    base: {
        color: "#32325D",
        fontWeight: 500,
        fontFamily: "Inter, Open Sans, Segoe UI, sans-serif",
        fontSize: "16px",
        fontSmoothing: "antialiased",

        "::placeholder": {
          color: "#CFD7DF"
        }
      },
      invalid: {
        color: "#E25950"
      },
  }

  var elements = stripe.elements();
  var cardElement = elements.create('card', {
    style: styles,
    hidePostalCode: true,
    });
  cardElement.mount('#example4-card');

  const form = document.querySelector('#new_donation');

  let donationType = '<%= donation_type %>'
  let donationAmount = 0;

  form.addEventListener('submit', function(e) {

  var formClass = '.example4';// + exampleName;
  var example = document.querySelector(formClass);
  example.classList.add('submitting');

    // Get  donation amount from form or params
    if (donationType == 'one_time') {
      const donationInputAmount = document.querySelector('#donation_input_amount').value;
      donationAmount = donationInputAmount;
    } else {
      donationAmount = '<%= subscription_donation_amount %>';
    }
    e.preventDefault();

    // Step 1: POST request to create payment intent
    fetch('/payment_intents', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        authenticity_token: '<%= form_authenticity_token %>',

        payment_intent: {
          status: "paid",
          donation_type: '<%= donation_type %>',
          amount: donationAmount,
          }
      }),
    })
    .then((response) => response.json())
    .then((paymentIntent) => {
      // Step 2: Create payment method and confirm payment intent.
      stripe.confirmCardPayment(
        paymentIntent.client_secret, {
          payment_method: {
            card: cardElement
          }
        }
      ).then((resp) => {
        if(resp.error) {
          alert(resp.error.message);
        } else {
          // Step 3: Embed payment ID in form
          const paymentIdInput = document.querySelector('#payment');
          paymentIdInput.value = paymentIntent.id;
          // Embed payment amount
          const donationOutputAmount = document.querySelector('#donation_amount');
          donationOutputAmount.value = donationAmount;
          example.classList.remove('submitting');
            example.classList.add('submitted');

          setTimeout(form.submit(), 3500);
        }
      })
    })
    .catch((error) => {
      console.error('Error:', error);
    });
  });

</script>
Enter fullscreen mode Exit fullscreen mode

To see the entire Rails view file go here: https://github.com/gperilli/fundme/blob/master/app/views/donations/new.html.erb

As you can see this is server side rendered Javascript with Ruby variables being pasted into the Javascript - the first time I had ever done something like this. I would normally avoid mixing two coding languages like this, but in this case I have coded it pretty quickly, and everything is in one Rails view file with no html - js file separation.

Setting up Stripe within the Rails project is as simple as installing the gem, creating an account with Stripe, and then putting the public and private keys in a .env file. My keys are held in my .env file, and set in the config/initializers/stripe.rb file.

So, they look like this:

PUBLISHABLE_KEY='pk_test_********'
SECRET_KEY='sk_test_********'
Enter fullscreen mode Exit fullscreen mode
Rails.configuration.stripe = {
  :publishable_key => ENV['PUBLISHABLE_KEY'],
  :secret_key      => ENV['SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]

Enter fullscreen mode Exit fullscreen mode

Please note the pk_test_and sk_test_ which stand for the test vesrions of the Stripe public key and secret key which you can get from your Stripe account. By using these in development you can test dummy payments using test credit card numbers such as 4444 4444 4444 4444. The pk_live_and sk_live_ versions should only be used in production environments.

The Javascript handling my payment form page also deals with the styling of the Stripe payment iframe sitting within my payment form:

var styles = {
    base: {
        color: "#32325D",
        fontWeight: 500,
        fontFamily: "Inter, Open Sans, Segoe UI, sans-serif",
        fontSize: "16px",
        fontSmoothing: "antialiased",

        "::placeholder": {
          color: "#CFD7DF"
        }
      },
      invalid: {
        color: "#E25950"
      },
  }
Enter fullscreen mode Exit fullscreen mode

For more information on styling see this: https://stripe.com/docs/js/appendix/style.

Stripe provide some ready-made payment forms whih are worth checking out here: https://stripe.dev/elements-examples/. These also have processing animations which are quite useful.

So, from this you can see that an email input can be omitted by simply not including the Stripe address element. Also worth noting here is the use of the Stripe card element which has more recently been superceded by the Stripe payment element.

Setting Up Subscription Payments Using Delayed Jobs in Rails with Sidekiq and Redis

The next major step I had in this project was creating an automated subscription feature. In terms of an engineering feet, time keeping and computing is quite interesting but I’ll probably never have to deal with that depth of engineering. However if you think about this for a second, any web service with a large number of subscribed users depends on a time keeping technology that can trigger code after a set interval. I can only imagine that the time keeping part of the code needs to stay on, all the time…and if it doesn’t, stuff breaks.

My initial thought as a programmer was: how can I mitigate against a situation when my subscription timers fail? And this problem still remains in my mind. If my web app needs to be restarted in the event of a serious issue, how can I fix this? Well, the only solution to this, I think, is to write recovery code that would look through all the users with a subscription and then calculate the next payment date based on the last - what a pain.

OK, so setting that potential issue aside for now, background or scheduled jobs in Rails need something like Sidekiq to handle the timing. In the Rails guides Sidekiq and others are called 3rd-party queuing libraries. Sidekiq needs to be installed as a gem and then run alongside the Rails server in order for Rails to execute timed events.

Currently, on my Ubuntu command line I use this to start up Sidekiq:

bundle exec sidekiq
Enter fullscreen mode Exit fullscreen mode

This outputs this to the terminal, and after this the enqueued execution logs get printed below:

               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   <| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|

Enter fullscreen mode Exit fullscreen mode

Lovely!

Rails jobs can be generated from the command line using:

rails generate job job_name
Enter fullscreen mode Exit fullscreen mode

These will then appear in the jobs directory. From within the Rails framework, the source code or the Rails console, the job can be executed using perform_nowor perform_later. The perform later will execute a Rails job after the specified interval.

In my app an automated payment job is executed after a monthly interval, incrementing the number of subscription payments by one each time until the maximum of 12 payments is reached. (In the code monthly_subscription_termis 12.) Upon reaching the maximum number of subscription payments - 12 - the user is unsubscribed. In order for this to work I created a callback function, trigger_next_subscription_donationon my Donationmodel which is executed if the user makes a payment (automated or otherwise). The callback function determines if a user should still be subscribed or not and then executes a delayed payment using the Donationinstance of the payment that was just made: ChargeSubscriberJob.set(wait: month_wait_time).perform_later(self). The self in that line is the Donation instance that is being passed to thr Rails job to generate the next automated payment.

def trigger_next_subscription_donation
    return unless subscription_id.present?

    this_subscription = Subscription.find_by!(id: subscription_id)
    if donation_type == 'monthly_subscription' || donation_type == 'automated'
      subscription_stage = user.subscription_stage + 1
      if subscription_stage < this_subscription.monthly_subscription_term + 1
        user.update_attribute(:subscription_stage, subscription_stage)
        month_wait_time = Rails.env.development? ? 1.minute : 1.month
        ChargeSubscriberJob.set(wait: month_wait_time).perform_later(self)
      elsif subscription_stage == this_subscription.monthly_subscription_term + 1
        # Stop Monthly Subscription
        user.update_attribute(:subscribed, false)
        user.update_attribute(:subscription_stage, 1)
        user.update_attribute(:subscription_frequency, '')
        user.donations.all.each do |donation|
          donation.update_attribute(:subscription_status, 'completed') if donation.subscription_status == 'active'
        end
        this_subscription.update_attribute(:status, 'completed')
      end
    elsif donation_type == 'yearly_subscription'
      # Stop Yearly Subscription
      year_wait_time = Rails.env.development? ? this_subscription.monthly_subscription_term.minutes : this_subscription.monthly_subscription_term.months
      EndYearlySubscriptionJob.set(wait: year_wait_time).perform_later(self)
    end
  end
Enter fullscreen mode Exit fullscreen mode

You can also see here that I’m using month_wait_time = Rails.env.development? ? 1.minute : 1.month because actually testing a month long interval is not feasible in the development environment. To see the entire Donation model, go here:

So that’s how a fairly simple subscription payment system was created. The last step in actually getting this to work in deployment is using Redis, an often extra paid-for service in heroku for example, that is providing data storage for Sidekiq. At this point in terms of the coding there is little more to say about Redis apart from the fact that it needs to be used as gem within the Rails framework, and configured as the Sidkiq adapter in whatever deployment you are using.

Top comments (0)