Được hàng triệu công ty sử dụng, Stripe cung cấp hạ tầng thanh toán cho ứng dụng xử lý subscription và mua một lần. Stripe Checkout cho phép dễ dàng chấp nhận thanh toán thẻ qua trang thanh toán được host, được thiết kế để tăng tỷ lệ chuyển đổi. Kết hợp với webhook cho phép developer bán sản phẩm và subscription rồi phân phối chúng dưới dạng số.
Mặc dù kỹ thuật có thể tự xử lý thanh toán dùng provider hiện có, Stripe mang lại nhiều lợi ích. Đầu tiên, nó nhanh hơn. Stripe có hơn 4.000 nhân viên làm việc để thanh toán đơn giản, an toàn, tiện lợi nhất có thể.
Hơn nữa, 'powered by Stripe' là cách dễ dàng xây dựng niềm tin khách hàng và tăng chuyển đổi. Khách hàng thường ngại cung cấp thông tin thẻ/ngân hàng cho mọi trang web, và đúng vậy.
Stripe không phải provider duy nhất. Paddle là player lớn khác. Paddle cung cấp nhiều tính năng tương tự Stripe, nhưng phí cao hơn một chút và developer experience anecdotally kém hơn. Mặc dù có vài competitor thay thế Stripe, sự phổ biến của sản phẩm thanh toán online khiến Stripe thành lựa chọn dễ dàng.
Thường các công ty SaaS xử lý subscription định kỳ, nhưng nhiều sản phẩm hưởng lợi từ bán một lần sản phẩm số. Điều này cũng dễ dàng với hạ tầng Stripe, nhưng tài liệu của họ dường như tránh chủ đề này.
Thiết lập
Để giải thích quy trình, chúng ta xây dựng ứng dụng cho phép người dùng trả phí để đăng quảng cáo trên bảng craigslist-style. Ví dụ cụ thể, ứng dụng là trang web tìm roommate. Người dùng duyệt bảng miễn phí nhưng phải trả phí để đăng quảng cáo riêng.
Tạo tài khoản Stripe
Đầu tiên, truy cập trang Stripe để tạo tài khoản. Đăng ký với thông tin cá nhân hoặc doanh nghiệp nếu áp dụng.
Screenshot trang đăng ký Stripe
Bài này không hướng dẫn chi tiết tạo tài khoản, nhưng tài liệu Stripe có thể trả lời câu hỏi. Chúng ta quan tâm đặc biệt đến sản phẩm payments and payouts hoặc cụ thể Stripe Checkout.
Tạo Rails app cơ bản
Sau khi tài khoản Stripe sẵn sàng, tạo Rails app cơ bản cho tích hợp. Nếu tích hợp app hiện có hoặc chỉ quan tâm phần Stripe, bỏ qua đến "Basic Stripe Integration".
Tôi dùng phiên bản sau:
- Rails 6.1
- Ruby 3.0.0
Giả sử Ruby và Rails đã cài, chạy:
rails new roommate-board
Tên app là roommate-board, bạn tự do chọn tên khác. Chỉ thay tên ở chỗ khác trong code ví dụ.
Vào thư mục vừa tạo:
cd roommate-board
Chạy app:
rails s
Nếu thấy trang chào Rails điển hình tại localhost:3000, chúc mừng, sẵn sàng code!
Trước hết, thêm Devise xử lý authentication. Thêm gem 'devise' vào Gemfile và chạy:
bundle install
Dùng generator Devise thiết lập:
rails generate devise:install
Kết nối Devise với User model:
rails generate devise User
Cuối cùng, chạy migration:
rails db:migrate
Tạo bảng Roommate
Nếu đọc đến đây, bạn quan tâm tích hợp Stripe hơn front-end, nên không lo styling.
Bước tiếp theo tạo model cho posting! Dùng generator Rails tạo model, migration, controller, routes. Chạy:
rails generate scaffold Post address:string rent:integer content:text
Model này chưa chứa tất cả cần cho Stripe. Chúng ta thêm sau nếu bạn tham khảo tutorial cho dự án không có đặc quyền bắt đầu từ đầu. Chạy migration tạo bảng database:
rails db:migrate
Tiếp, làm index view cho posts thành root cho tiện. Trong config/routes.rb, thêm:
root 'posts#index'
Hiện khá trống, không có cách rõ ràng sign in/out/up. Chức năng đã có, chỉ thêm link trong header. Trong app/views/layouts/application.html.erb, thêm link cần thiết. File của tôi trông như sau sau thay đổi:
<!DOCTYPE html>
<html>
<head>
<title>RoommateBoard</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<header>
<nav>
<a href="/#" class="block">
<h1>Roommate Board</h1>
</a>
<% if user_signed_in? %>
<%= link_to "Manage Posts", manage_posts_url %>
<%= link_to "Sign Out", destroy_user_session_url, :method => 'delete' %>
<% else %>
<%= link_to "New Post", new_user_registration_url %>
<% end %>
</nav>
</header>
<%= yield %>
</body>
</html>
Đây cho link trang chủ, khi click "New Post" chuyển trang sign-up. Nếu signed in, đổi thành "Manage Posts" và "Sign Out". Không styling và chưa sign in, trang root trông như:
Screenshot trang index posts cơ bản
Tiếp, scope mỗi posting cho user duy nhất. Tạo association belongs_to giữa Posting và User, cùng logic liên quan. Bắt đầu generator migration Rails; chạy:
rails g migration AddUserIdToPosts
Sửa file migration trong db/migrate/ như sau:
class AddUserIdToPosts < ActiveRecord::Migration[6.1]
def change
add_column :posts, :user_id, :integer
end
end
Cuối cùng, chạy migration:
rails db migrate
Tiếp, thêm associations vào models. Trong app/models/post.rb, thêm:
belongs_to :user
Trong app/models/user.rb, thêm:
has_many :posts
Association hai chiều cho method Rails tiện lợi, như sắp dùng trong new và create. Vào app/controllers/posts_controller.rb xem method new. Thay @post = Post.new bằng @post = current_user.posts.build.
Trong method create, làm tương tự. Thay @post = Post.new(post_params) bằng @post = current_user.posts.build(post_params). Cùng nhau, new và create trông như:
# GET /posts/new
def new
@post = current_user.posts.build
end
# POST /posts or /posts.json
def create
@post = current_user.posts.build(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Post was successfully created." }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
Điều này đảm bảo user_id lưu trong mọi post, nhưng không ngăn user khác edit/xóa post không của họ! Trong PostsController, viết method cho Boolean indicator user có sở hữu posting. Thêm cuối controller:
def user_owns_post?
@post.user == current_user
end
Trong method edit, thêm (sẽ là code duy nhất):
unless user_owns_post?
# Redirect to error page
redirect_to posts_path, flash: { error: "You are not the owner of that post!" }
end
Trong method destroy, thêm đầu:
unless user_owns_post?
# Redirect to error page
redirect_to posts_path, flash: { error: "You are not the owner of that post!" }
end
Tiếp, cần chức năng quản lý posts hiện có. Mục tiêu: user tạo post ở trạng thái draft trước khi thanh toán. Khi thanh toán, set active! Trong PostsController, thêm method mới:
def manage
@posts = current_user.posts
end
Thêm route để user truy cập! Trong config/routes.rb, thêm:
get "/manage_posts/" =>'posts#manage'
Cuối, tạo view quản lý post! Thêm file app/views/posts/manage.html.erb. Copy toàn bộ nội dung app/views/posts/index.html.erb vào. Chỉ đổi header từ "Posts" thành "Manage Posts". Đây về cơ bản là index page nhưng chỉ index posts của user nhờ logic controller. Kỹ thuật có thể làm trên index page, nhưng tách biệt dễ thêm chức năng sau. Ví dụ, thêm link tạo post mới trên trang quản lý.
Dưới header, thêm:
<%= link_to "New Post", new_post_url%>
Đây là tất cả setup! Tốt nên kiểm tra mọi thứ hoạt động. Bây giờ bạn nên:
- Tạo tài khoản user
- Sign in
- Sign out
- Xem index các posting hiện có
- Tạo posting mới
Nếu mọi thứ hoạt động như kỳ vọng, chuyển sang tích hợp Stripe!
Tích hợp Stripe
Thêm Stripe Ruby Gem
Đầu tiên, thêm Stripe Ruby gem. Trong Gemfile, thêm:
gem 'stripe'
Chạy:
bundle install
Phần sau cần Stripe API key. Trên Stripe console, click "Developer" sidebar. Option "API Keys" hiện ra, click. Ghi nhớ "Secret Key", cần ngay.
Nhưng trước, tạo initializer trong config/initializers. Tạo stripe.rb. Thêm dòng sau (thay secret key):
Stripe.api_key = <insert your key here as a string>
Lưu trữ thông tin Stripe
Cách lưu credentials này không an toàn. Không nên lưu production secrets dạng plaintext, nơi bị theo dõi bởi git và ai đọc codebase đều thấy. May mắn Rails cung cấp cách lưu credentials an toàn.
Trong shell, chạy EDITOR=vim bin/rails credentials:edit để decrypt và mở file credentials trong vim. Nhấn 'i' để chuyển insert mode. Thêm section như sau:
stripe:
secret: your-secret-key
public: your-public-key
Lưu file thoát vim. Cách vài cách, yêu thích: nhấn Escape (thoát insert mode) rồi gõ :wq enter. Nếu muốn biết thêm cách Rails xử lý encrypted credentials, tài liệu này hay.
Bây giờ credentials an toàn, thay code initializer không an toàn bằng:
Stripe.api_key = Rails.application.credentials[:stripe][:secret]
Liên kết đến Stripe Checkout từ sản phẩm
Có vài cách tận dụng Stripe cho thanh toán, nhưng tùy chọn hay là Stripe Checkout. Stripe Checkout là hosted payment page, nghĩa là bạn không tạo UI cho giao dịch thực tế. User click button, redirect đến Stripe, rồi redirect về app sau thanh toán. Làm quản lý context hơi thách thức, nhưng gánh nặng tạo payment form bị bỏ và Stripe làm nhiều việc tối đa chuyển đổi. Bạn dễ chấp nhận loại thanh toán ngoài thẻ, bao gồm ACH hoặc nhiều phương thức quốc tế.
Đầu tiên, tạo controller xử lý checkout. Tạo app/controllers/checkout_controller.rb. Viết method xử lý tạo checkout session.
def create
@session = Stripe::Checkout::Session.create({
success_url: root_url,
cancel_url: manage_posts_url,
payment_method_types: ['card'],
line_items: [{
name: "Roommate Posting",
amount: 2000,
currency: "usd",
quantity: 1
}],
mode: 'payment',
metadata: {post_id: params[:post_id]},
customer_email: current_user.email,
success_url: manage_posts_url,
cancel_url: manage_posts_url
})
respond_to do |format|
format.js
end
end
Tạo một Checkout Session với context cần thiết cho Stripe. Truyền single line item, hard-code vì app chỉ có một sản phẩm. Dễ dàng dùng parameter truyền vào để customize checkout cho sản phẩm cụ thể. Biến "Amount" là chi phí sản phẩm tính bằng cent. Cũng đáng nói truyền email user hiện tại cho đơn giản. Điều quan trọng cuối: truyền id post như metadata. Sẽ dễ dàng fulfill mua hàng của user trong phần webhook!
Tiếp, cần cách gọi method.
Trong config/routes.rb, thêm dòng tạo route đến controller:
post 'checkout/create' => 'checkout#create', as: "checkout_create"
Tiếp, thêm button thanh toán cho post bất kỳ trong manage posts view. Trong app/views/manage.html.erb, thêm cột header thêm cuối:
<th>Payment</th>
Cũng chuyển <th colspan="3"></th> thành <th colspan="4"></th>.
Thêm item trong table body cho payment button. Là item thứ tư trong body, thêm:
<td><%= button_to "Submit Payment", checkout_create_path, params: {:post_id => post.id }, remote: true %></td>
Tóm lại, manage posts view giờ như:
<p id="notice"><%= notice %></p>
<h1>Manage Posts</h1>
<%= link_to "New Post", new_post_url%>
<table>
<thead>
<tr>
<th>Address</th>
<th>Rent</th>
<th>Content</th>
<th>Payment</th>
<th colspan="4"></th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.address %></td>
<td><%= post.rent %></td>
<td><%= post.content %></td>
<td><%= button_to "Submit Payment", checkout_create_path, params: {:post_id => post.id }, remote: true %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
JavaScript
Đừng quên thêm Stripe JavaScript package include vào app/views/application.html.erb với dòng:
<script src="https://js.stripe.com/v3/"></script>
Tiếp, thêm file (và thư mục!) mới app/views/checkout/create.js.erb. Thêm sau để checkout session tạo khi click button nhờ controller:
var stripe = Stripe("<%= Rails.application.credentials[:stripe][:public] %>")
stripe.redirectToCheckout({
sessionId: '<%= @session.id %>'
}).then(function (result) {
console.log(result.error_message)
});
Thiết lập Webhooks
Giờ có cách user trả tiền cho post! Tuy nhiên, chưa có cách app biết post đã trả tiền hay activate nó. Đầu tiên, thêm Boolean vào post model chỉ ra post đã thanh toán. Dùng generator migration Rails, chạy:
rails g migration AddPaymentDetailsToPost
Mở migration thêm dòng:
add_column :posts, :is_paid, :boolean, :default => false
Thêm attribute/cột is_paid vào posts table/model. Attribute Boolean mặc định false, nghĩa là khi tạo post, đánh dấu chưa thanh toán. Khi post thanh toán, flip Boolean thủ công. Nhưng trước chạy migration:
rails db:migrate
Vì thanh toán không xử lý tức thì, không thể dựa vào response API thành công từ Stripe xác định job đã thanh toán. Thay vào đó, chỉ định endpoint trên app để Stripe POST request khi thanh toán xong xử lý. Quy trình này gọi là webhooks và thực sự đơn giản hơn nghe!
Đầu tiên, tạo controller mới trong app/controllers/ tên WebhooksController. Trong app/controllers/webhooks_controller.rb, viết:
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload,
sig_header,
Rails.application.credentials[:stripe][:webhook]
)
rescue JSON::ParserError => e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
puts "Signature error"
p e
return
end
# Handle the event
case event.type
when 'checkout.session.completed'
session = event.data.object
post = Post.find_by(id: session.metadata.post_id)
post.is_paid = true
post.save!
end
render json: { message: 'success' }
end
end
Ngoài boilerplate cần thiết, method xử lý event type cụ thể checkout.session.completed. Còn lại phía Rails là thêm route vào config/routes.rb:
resources :webhooks, only: [:create]
Cuối, cần kết nối tài khoản Stripe với endpoint này trên Stripe dashboard. Vì cung cấp URL cho dashboard, sẽ không chạy local! Phải deploy đến endpoint accessible internet để webhooks hoạt động.
Quay lại "Developers" trên Stripe dashboard, lần này chọn "Webhooks" panel trái. Phần "Endpoints", click "Add Endpoint". Cung cấp URL app append /webhooks và gán event "checkout.session.completed".
Đó là xong tích hợp Stripe! Bước thực tế cuối cho app mock là index page chỉ hiển thị posts đã thanh toán. Dễ làm trong index method của app/controllers/posts_controller.rb. Đổi method thành:
def index
@posts = Post.where(:is_paid => true)
end
Giờ user tạo post, thanh toán qua Stripe checkout, tự động hiển thị trên homepage!
Chuyển sang Production
Vì webhooks chỉ chạy khi deploy live site, đáng thảo luận thay đổi cần cho deployment. Đầu tiên, communication phải HTTPS để Stripe checkout chạy. Nếu gặp vấn đề, check JavaScript console để xem hint từ Stripe.
Có khả năng setup ban đầu dùng test keys của Stripe. Nếu vậy, API Keys trên dashboard trông như:
Screenshot Stripe dashboard API Keys
Chỉ click toggle "viewing test data" để hiện production secret và public keys. Cần mở Rails credentials manager như trước và thay test keys bằng production keys cho app xử lý live data. Môi trường test hữu ích chạy test transaction với số thẻ giả. Nếu nhiều environment hoặc chuyển đổi thường xuyên, đáng tạo key/value pair khác trong credentials manager và dùng động trong code.
Kết luận
Chúng ta dành nhiều thời gian tạo app ví dụ và không lâu tích hợp Stripe. Nhờ Stripe Checkout, chúng ta chuyển giao nhiều trách nhiệm, bao gồm UI, tiết kiệm code. Tạo trải nghiệm nhất quán cho user, nhờ sự phổ biến của Stripe trong marketplace online. Sứ mệnh Stripe là tăng GDP của internet, cần app như của bạn. Vì vậy, họ luôn tìm cách dễ dàng hơn xử lý thanh toán. Stripe Checkout khá dễ triển khai, nhưng họ mới nghĩ ra cách nhanh hơn.
Stripe Payment Links là sản phẩm mới từ Stripe có tiềm năng làm tích hợp Checkout gần như không cần thiết. Tất cả chúng ta làm là redirect user đến hosted checkout page, và một link đơn giản có tiềm năng làm dễ dàng hơn. Payment links là giải pháp no-code dường như nhắm retailer nhưng có thể dùng cho trường hợp này. Tuy nhiên, Stripe Checkout vẫn linh hoạt và phổ biến hơn, nên quan trọng hiểu kỹ.
Vấn đề nhiều doanh nghiệp gặp là xử lý thuế, đặc biệt quốc tế. Stripe mới giới thiệu tính năng tự động xử lý, nhưng vẫn beta. Khi có sẵn, sẽ giảm gánh nặng kế toán cho người dùng Stripe!
Tài liệu Stripe và bài viết dành cho Ruby on Rails thiên về subscription hơn mua một lần. Subscriptions là recurring charges gắn với user và product. Phía Stripe, triển khai giống mua một lần. Phía app, phải tích hợp subscription management, cho user quản lý subscription, kiểm tra trạng thái định kỳ. Bài này tập trung mua một lần, nhưng nhiều tài nguyên cộng đồng nếu cần quản lý subscription.