- create a new rails application named
subscription-app
rails new subscription-app -d mysql- Enter your application
cd subscription-appIf you setup MySQL or Postgres with a username/password, modify the
config/database.ymlfile to contain the username/password that you specified
- Make sure that MySQL server is running. Just type
mysql. If you get an error like this:
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)It's most likely that MySQL server is not running. You can start it with:
sudo service mysql start- Generate the database
rake db:create- Open your project in Visual Studio Code. You can type
explorer.exe .to open the project on windows (for BashOnUbuntuOnWindows).
- Devise provides a number of user routes automatically.
- Session routes for Authenticatable (default):
| User/scope | Method | Route | Controller Action |
|---|---|---|---|
| new_user_session | GET | /users/sign_in | devise/sessions#new |
| destroy_user_session | DELETE | /users/sign_out | devise/sessions#destroy |
| new_user_registration | GET | /users/sign_up | devise/registrations#new |
For other available routes, look at SessionsController and RegistrationsController
- URL helpers are provided to be used with resource/scope. They act like proxies to the generated routes created by devise. Example:
| User/scope | URL helper |
|---|---|
| new_user_session | new_user_session_path |
| destroy_user_session | destroy_user_session_path |
| new_user_registration | new_user_registration_path |
- Devise provides a number of user helpers automatically.
Roles: User Admin
Generated methods: authenticate_user! # Signs user in or redirect authenticate_admin! # Signs admin in or redirect user_signed_in? # Checks whether there is a user signed in or not admin_signed_in? # Checks whether there is an admin signed in or not current_user # Current signed in user current_admin # Current signed in admin user_session # Session data available only to the user scope admin_session # Session data available only to the admin scope
Use: before_action :authenticate_user! # Tell devise to use :user map before_action :authenticate_admin! # Tell devise to use :admin map
- Users have an email and password
- Use Devise for drop-in authentication
- Devise includes modules for additional functionality: Encrypt password, OmniAuth, Email confirmation, Recover password, Lock account, Expire session.
- Devise configures the user table with same default columns.
| Name | Type |
|---|---|
| id | integer |
| character varying | |
| encrypted_password | character varying |
| sign_in_count | integer |
| current_sign_in_at | timestamp without time zone |
| created_at | timestamp without time zone |
| updated_at | timestamp without time zone |
- For instructions on installing Devise go to getting started
Small recap:
gem 'devise'inGemfile$ bundle install$ rails generate devise:installconfig.action_mailer.default_url_options = { host: 'localhost', port: 3000 }inconfig/environments/development.rbroot to: "home#index"inconfig/routes.rb- Create and update
home_controller.rb - Create and update
views/home/index.html.erb rails server(ensure that MySQL server is running)- Update
views/layouts/application.html.erbto include<p class="notice"><%= notice %></p>and<p class="alert"><%= alert %></p> $ rails generate devise User$ rake db:migraterails serverif your application is nor running yet
http://localhost:3000/users/sign_inandhttp://localhost:3000/users/sign_upshould be available and functional.
- Add Bootstrap CDN and
<%= render 'nav' %>inviews/layouts/application.html.erb - Create and update
views/application/_nav.html.erb
- The Publication Model:
- The digital file subscribers will have access to
- "Lightweight" model as it is not very relevant to implementing payments
- Should be customized based on your needs
- Publication scheme:
| Name | Type |
|---|---|
| id | integer |
| title | character varying |
| file_url | character varying |
| description | character varying |
| created_at | timestamp without time zone |
| updated_at | timestamp without time zone |
-
Administrators:
- Administrators are users with
is_adminset to true - A initial administrator will be created, with the option to add more via the console
- Many Ruby gems exist for handling administrators :
cancan,pundit,rolify
- Administrators are users with
-
Administrator and Subscriber views:
- Administrators can create, edit, and update publications
- Subscribers can only view publications
- Non-subscribers can see publications but not access their details
Publication Routes (Admin):
| Method | Route | Controller Action |
|---|---|---|
| GET | /admin/publications | admin/publications#index |
| GET | /admin/publications/:id | admin/publications#show |
| GET | /admin/publications/new | admin/publications#new |
| POST | /admin/publications | admin/publications#create |
| GET | /admin/publications/:id/edit | admin/publications#edit |
| PUT | /admin/publications/:id | admin/publications#update |
| DELETE | /admin/publications/:id | admin/publications#destroy |
Publication Routes (Subscriber):
| Method | Route | Controller Action |
|---|---|---|
| GET | /publications | publications#index |
| GET | /publications/:id | publications#show |
Publication Routes (Non-subscriber):
| Method | Route | Controller Action |
|---|---|---|
| GET | /publications/:id | publications#show |
- Create the publication model
rails g model publication title:string description:text file_url:string- Migrate the database to generate the publication model:
rake db:migrate- Add routes in
config/routes.rb:
resources :publications, only: [:index, :show]- Create the
publications_controller.rband theindexandshowviews. - To add a publication for test you can type:
rails c
p = Publication.new(title: 'My first publication', description: 'This is my first publication', file_url: 'http://myfilelocation.com')
p.save
exit- Create a migration file to add the
is_admincolumn to theuserstable to allow users to become admin:
rails g migration add_is_admin_to_users is_admin:boolean- Modify the generated migration file for
add_is_admin_to_usersto set null to false and default to false.
add_column :users, :is_admin, :boolean, null: false, default: false- Run the migration:
rake db:migrate- You can test everything with the rails console:
rails c
User.last
User.last.is_admin
User.last.update(is_admin: true)- If you wish, you can set an admin in the
seeds.rb(so you have one by default):
User.create(
email: 'admin@test.com',
password: 'password',
is_admin: true
)- Update the
routes.rbto add the admin routes. - Create the
admin_controller.rbunder controllers. - Add the
adminfolder under controllers, and add thepublications_controller.rb. - Add the
adminfolder under views and, underadminfolder, thepublicationsfolder and add theindex.html.erb,edit.html.erb,show.html.erb,new.html.erb. - Run
rails sand navigate tolocalhost:3000/admin/publicationsto test the admin routes.
- We already did at
localhost:3000/publications. You can check that everything works fine.
- Payment processors:
PayPal,Authorize.Net,Stripe. - Why Stripe?
- PCI compliance is hard
- Powerful API
- Low cost
- Great support
- Stripe's Subscription API
- Allows recurring charges on a schedule
- Handles storing credit card information
- Additional features like coupons and trial periods are also available
-
Adding Stripe to Our application
- Add the Stripe API gem
- Find our Stripe API keys
- Use
dotenvto securely store API keys - Test the API in the console
-
Go to Stripe and open an account.
-
Create a new product in the
Productssection. Example:
Subscription App Bronze Plan 9.00 $ / month
- Install Stripe libraries and tools
# If you use bundler, you can add this line to your Gemfile
gem 'stripe'- Run
bundle install - Create
config/initializers/stripe.rband paste the Stripe.api_key
Stripe.api_key = 'sk_test_XXXXXXXXXXXXXXXXXXXXXX'- To retrieve the subscription, you need to retrieve the plan using the prices API ID.
- You can test that everything works fine using the rails console:
rails c
Stripe::Plan.retrieve('price_XXXXXXXXXXXXXXXXXX')
exitNotice: be sure to use your
API keyand theprice_idof your product (if you are logged in, the provided examples should already have your keys and price_id).
- Add
dotenv. A.envfile can keep the API keys out of the code. - Add the API key in a dotenv file.
- Add
gem 'dotenv-rails', groups: [:development, :test]to Gemfile and runbundle install. - Create
.envfile and addSTRIPE_API_KEY="XXXXXXXXXXX". Make sure that.envis in.gitignore. - Update
stripe.rb:Stripe.api_key = ENV["STRIPE_API_KEY"]. - Test again in your console that everything is still working:
rails c
Stripe.api_key
exit-
Tied to a user
-
activeboolean -
Stores Stripe user_id for retrieval
-
Create the subscription model:
rails generate model subscription stripe_user_id:string active:boolean user:references- Update
create_subscriptions.rbmigration file to ensureactivecan't be null and is false by default:
t.boolean :active, null: false, default: false- Add the subscription relation to
user.rbmodel:
has_one :subscription- Make sure that the user relation is set in
subscription.rbmodel:
belongs_to :user-
Run
rake db:migrate -
Add an active record callback in the
user.rbmodel to generate a default subscription to every new user:
after_create :create_subscription
def create_subscription
Subscription.create(user_id: id) if subscription.nil?
end- Add a new migration to ensure previous users also have a subscription:
rails g migration AddSubscriptionToPreviousUsers- Update the
add_subscription_to_previous_users.rbmigration file with theupfunction. More details at active record migrations:
def up
User.all.each do |user|
user.create_subscription
end
end- Run
rake db:migrate
We should see that a subscription is created for every existing user.
- You can also verify with rails console:
rails c
User.last.subscription
exit- Generate a users controller as well as the
inforoute where users can manage their account information:
rails generate controller users info- Update
routes.rbto indicate the controller's actionusers#info:
get '/users/info', to: 'users#info'- Update
users_controller.rbto add the devise helperauthenticate_userand the@subscriptionparameter to use it in the view:
before_action :authenticate_user!
def info
@subscription = current_user.subscription
end- Update
_nav.html.erbto add a link tousers_info_path:
<% if user_signed_in? %>
<li class="nav-item">
<%= link_to current_user.email, users_info_path %>
</li>- Update
app/views/users/info.html.erbto show if the user issubscribedorunsubscribed:
<%= current_user.email %>
<% if @subscription.active %>
subscribed
<% else %>
unsubscribed
<% end %>
- Navigate to
localhost:3000/users/infoto see the new changes.
- Client-side JavaScript library
- Credit card information never touches our servers
- A Stripe
tokenis used to communicate back and forth - Gather credit card information in a form
- Send it directly to Stripe from the browser
- Recommended by Stripe
- Local Subscription model becomes super light-weight
- Using the Stripe token, we create a subscription with the Stripe Ruby library
- The corresponding user_id from Stripe is stored locally for easy retrieval
Have a look at Create fixed-price subscriptions for subscriptions implementation.