Getting started with Stripe, Rails and React in three easy steps
A client recently requested to add the functionality to be able to accept credit card payments within a Rails application that we were building. Specifically, they wanted users to be able to purchase vouchers using credit card payments and pass on any processing fees. Once the payment was complete, the transaction had to be recorded within the app and an email needed to be sent out to both the purchaser and the giftee. Stripe came into the picture then.
Stripe offers payment processing software for websites and mobile applications. Their APIs and documentation make it super simple to set up and accept credit card payments whilst complying with regulations. Credit card information never touches our app and is directly sent to Stripe. The steps to setup Stripe in our app was pretty straightforward and I will cover it below.
Step One
We had to set up an endpoint within our Rails app - this would allow us to create what Stripe calls a “payment intent”. A payment intent represents a single customer session within Stripe. Stripe recommends you create a payment intent as soon as you know the payment amount. The API we created required the client to pass through an amount and some metadata used to populate information for our voucher. The Rails app then calculated the fee charged by Stripe, added it onto the total amount, created a payment intent within Stripe and returned back the payment intent’s client_secret. This is what the UI will now use to interface directly with Stripe’s APIs.
class CreatePaymentIntent
class InvalidAmountError < StandardError; end
class InvalidEmailError < StandardError; end
include UseCase
attr_reader :payment_intent, :metadata
def initialize(amount:, from_name:, to_name:, from_email:, to_email:, note: nil)
@amount = amount
@from_name = from_name
@to_name = to_name
@from_email = from_email
@to_email = to_email
@note = note
end
def perform
validate_emails
validate_payment_amount
determine_processing_fee_and_payment_amount
determine_total_payment_amount_in_cents
create_payment_intent!
rescue InvalidEmailError
nil
rescue InvalidAmountError => e
errors.add(:amount, e.message)
rescue StandardError => e
errors.add(:base, e.message)
end
private
# Check if the from and to email addresses are valid
def validate_emails
end
# Check the payment amount is between the minimum voucher amount and maximum voucher amount
def validate_payment_amount
end
# Calculate the processing fee and payment amount
def determine_processing_fee_and_payment_amount
# See: https://support.stripe.com/questions/passing-the-stripe-fee-on-to-customers
@payment_amount = (@amount + ApplicationConfig::STRIPE_PROCESSING_FEE_CHARGE) / (1 - (ApplicationConfig::STRIPE_PROCESSING_FEE_PERCENTAGE / 100))
@processing_fee = @payment_amount - @amount
end
# Calculate the payment amount in cents
def determine_total_payment_amount_in_cents
end
def create_payment_intent!
@payment_intent = Stripe::PaymentIntent.create(amount: @payment_amount_in_cents, currency: ApplicationConfig::STRIPE_CURRENCY, metadata: construct_metadata)
end
# Build a hash representation with the amount, processing fee, from_name, to_name, from_email, to_email and note
def construct_metadata
end
end
Step Two
After creating a payment intent, we set up a simple checkout form. This was done using the React Stripe.js library, the Elements provider and components and two hooks (useElements and useStripe). The form used the client_secret from step one as well as an API key which can be obtained from the Stripe console. You’re also able to style these components how you like.
Step Three
After setting up the checkout form, we set up a webhook endpoint within our Rails app. Once a payment has been successfully processed, Stripe sends a payment_intent.succeeded
event. We configured the webhook endpoint within the Stripe dashboard to get Stripe to send events to. This webhook would issue a voucher and then email the purchaser and the recipient. Stripe also provides other events such as when a payment is created and fails.
class HandleStripeEvent
class UnsupportedEventError < StandardError; end
PAYMENT_INTENT_SUCCEEDED_EVENT_TYPE = 'payment_intent.succeeded'
include UseCase
attr_reader :event
def initialize(payload:, signature:)
@payload = payload
@signature = signature
end
def perform
construct_event
construct_metadata
construct_payment_provider_metadata
add_job_to_queue
rescue JSON::ParserError
errors.add(:event, I18n.t('use_cases.handle_stripe_event.json_parse_error'))
rescue Stripe::SignatureVerificationError
errors.add(:event, I18n.t('use_cases.handle_stripe_event.invalid_signature_error'))
rescue UnsupportedEventError => e
errors.add(:event, e.message)
end
private
def construct_event
@event = Stripe::Webhook.construct_event(@payload, @signature, endpoint_secret)
end
# Build a hash representation of the metadata from the event data
def construct_metadata
end
# Build payment provider metadata hash of Stripe event identifier (from the event id) and the Stripe payment intent identifier (from the event data object id)
def construct_payment_provider_metadata
end
# Add payment intent succeeded events received to the IssueVoucherJob queue to process the event asynchronously with the basic event metadata and the payment provider metadata
# Raise unsupported event error for any other event types
def add_job_to_queue
end
# Stripe endpoint secret value
def endpoint_secret
end
end
Voila - that’s the Stripe checkout flow complete. From here, the Stripe dashboard offers excellent visibility over payments that have been processed, or are in progress. It also offers fantastic tools to visualise events that have been sent and the response received from your APIs.
We barely scratched the surface of what Stripe offers - Stripe has the ability to handle invoices, plans, quotes, subscriptions and more. We found that the combination of the payment intents API and their component library meant we could get this feature up and running fast, with minimal configuration.
Get in touch today, if you have an application that may need to accept credit card payments in future.