4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/bootstrap_and_overrides.css.scss:
--------------------------------------------------------------------------------
1 | @import "bootstrap";
2 | body { padding-top: 60px; }
3 | @import "bootstrap-responsive";
4 |
--------------------------------------------------------------------------------
/db/migrate/20121221192543_add_name_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddNameToUsers < ActiveRecord::Migration
2 | def change
3 | add_column :users, :name, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/role.rb:
--------------------------------------------------------------------------------
1 | class Role < ActiveRecord::Base
2 | has_and_belongs_to_many :users, :join_table => :users_roles
3 | belongs_to :resource, :polymorphic => true
4 |
5 | scopify
6 | end
7 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run RailsRecurlySubscriptionSaas::Application
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | RailsRecurlySubscriptionSaas::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/initializers/recurly.rb:
--------------------------------------------------------------------------------
1 | Recurly.api_key = ENV['RECURLY_API_KEY']
2 | Recurly.js.private_key = ENV['RECURLY_JS_PRIVATE_KEY']
3 | RECURLY_SUBDOMAIN = ENV['RECURLY_SUBDOMAIN']
4 | # Recurly.default_currency = 'USD'
--------------------------------------------------------------------------------
/app/assets/stylesheets/home.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the home controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | class UserMailer < ActionMailer::Base
2 | default :from => "notifications@example.com"
3 |
4 | def expire_email(user)
5 | mail(:to => user.email, :subject => "Subscription Cancelled")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/user_mailer/expire_email.text.erb:
--------------------------------------------------------------------------------
1 | Subscription Cancelled
2 |
3 | Your subscription has been cancelled.
4 |
5 | We are sorry to see you go. We'd love to have you back.
6 | Visit example.com anytime to create a new subscription.
7 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/home.js.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
4 |
--------------------------------------------------------------------------------
/spec/controllers/home_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe HomeController do
4 |
5 | describe "GET 'index'" do
6 | it "should be successful" do
7 | get 'index'
8 | response.should be_success
9 | end
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20121221193007_changes_for_recurly_to_users.rb:
--------------------------------------------------------------------------------
1 | class ChangesForRecurlyToUsers < ActiveRecord::Migration
2 | def change
3 | change_table :users do |t|
4 | t.rename :name, :first_name
5 | t.string :last_name
6 | t.string :customer_id
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | RailsRecurlySubscriptionSaas::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/app/views/layouts/_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% flash.each do |name, msg| %>
2 | <% if msg.is_a?(String) %>
3 |
7 | <% end %>
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/features/users/user_edit.feature:
--------------------------------------------------------------------------------
1 | Feature: Edit User
2 | As a registered user of the website
3 | I want to edit my user profile
4 | so I can change my username
5 |
6 | Scenario: I sign in and edit my account
7 | Given I am logged in
8 | When I change my email address
9 | Then I should see an account edited message
10 |
--------------------------------------------------------------------------------
/script/cucumber:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
4 | if vendored_cucumber_bin
5 | load File.expand_path(vendored_cucumber_bin)
6 | else
7 | require 'rubygems' unless ENV['NO_RUBYGEMS']
8 | require 'cucumber'
9 | load Cucumber::BINARY
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/pricing.css.scss:
--------------------------------------------------------------------------------
1 | .plans{
2 | text-align: center;
3 | }
4 | .featured{
5 | -webkit-transform:scale(1.15);
6 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.67);
7 | }
8 | .plan{
9 | background-color: #111575;
10 | }
11 | .plan.featured-plan{
12 | background-color: #CCAB00;
13 | }
14 | .plan h2{
15 | line-height: 100px;
16 | color: #fff;
17 | }
18 |
--------------------------------------------------------------------------------
/config/initializers/rolify.rb:
--------------------------------------------------------------------------------
1 | Rolify.configure do |config|
2 | # By default ORM adapter is ActiveRecord. uncomment to use mongoid
3 | # config.use_mongoid
4 |
5 | # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false
6 | # Enable this feature _after_ running rake db:migrate as it relies on the roles table
7 | # config.use_dynamic_shortcuts
8 | end
--------------------------------------------------------------------------------
/features/users/sign_out.feature:
--------------------------------------------------------------------------------
1 | Feature: Sign out
2 | To protect my account from unauthorized access
3 | A signed in user
4 | Should be able to sign out
5 |
6 | Scenario: User signs out
7 | Given I am logged in
8 | When I sign out
9 | Then I should see a signed out message
10 | When I return to the site
11 | Then I should be signed out
12 |
--------------------------------------------------------------------------------
/spec/factories/users.rb:
--------------------------------------------------------------------------------
1 | # Read about factories at https://github.com/thoughtbot/factory_girl
2 |
3 | FactoryGirl.define do
4 | factory :user do
5 | first_name 'Test'
6 | last_name 'User'
7 | email 'example@example.com'
8 | password 'changeme'
9 | password_confirmation 'changeme'
10 | # required if the Devise Confirmable module is used
11 | # confirmed_at Time.now
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/ability.rb:
--------------------------------------------------------------------------------
1 | class Ability
2 | include CanCan::Ability
3 |
4 | def initialize(user)
5 | user ||= User.new # guest user (not logged in)
6 | if user.has_role? :admin
7 | can :manage, :all
8 | else
9 | can :view, :silver if user.has_role? :silver
10 | can :view, :gold if user.has_role? :gold
11 | can :view, :platinum if user.has_role? :platinum
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
2 | <%%= f.error_notification %>
3 |
4 |
13 | <%% end %>
14 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/app/views/devise/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 |
12 | We are sorry to see you go. We'd love to have you back.
13 | Visit example.com anytime to create a new subscription.
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/controllers/content_controller.rb:
--------------------------------------------------------------------------------
1 | class ContentController < ApplicationController
2 | before_filter :authenticate_user!
3 |
4 | def silver
5 | authorize! :view, :silver, :message => 'Access limited to Silver Plan subscribers.'
6 | end
7 |
8 | def gold
9 | authorize! :view, :gold, :message => 'Access limited to Gold Plan subscribers.'
10 | end
11 |
12 | def platinum
13 | authorize! :view, :platinum, :message => 'Access limited to Platinum Plan subscribers.'
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/jquery.readyselector.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | var ready = $.fn.ready;
3 | $.fn.ready = function (fn) {
4 | if (this.context === undefined) {
5 | // The $().ready(fn) case.
6 | ready(fn);
7 | } else if (this.selector) {
8 | ready($.proxy(function(){
9 | $(this.selector, this.context).each(fn);
10 | }, this));
11 | } else {
12 | ready($.proxy(function(){
13 | $(this).each(fn);
14 | }, this));
15 | }
16 | }
17 | })(jQuery);
18 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | RailsRecurlySubscriptionSaas::Application.routes.draw do
2 | get "recurly/test"
3 | post "recurly/push"
4 | get "content/gold"
5 | get "content/silver"
6 | get "content/platinum"
7 | authenticated :user do
8 | root :to => 'home#index'
9 | end
10 | root :to => "home#index"
11 | devise_for :users, :controllers => { :registrations => 'registrations' }
12 | devise_scope :user do
13 | put 'update_plan', :to => 'registrations#update_plan'
14 | end
15 | resources :users
16 | end
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 |
3 | def display_base_errors resource
4 | return '' if (resource.errors.empty?) or (resource.errors[:base].empty?)
5 | messages = resource.errors[:base].map { |msg| content_tag(:p, msg) }.join
6 | html = <<-HTML
7 |
8 |
9 | #{messages}
10 |
11 | HTML
12 | html.html_safe
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | RailsRecurlySubscriptionSaas::Application.config.session_store :cookie_store, key: '_rails-recurly-subscription-saas_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # RailsRecurlySubscriptionSaas::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/config/cucumber.yml:
--------------------------------------------------------------------------------
1 | <%
2 | rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
3 | rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
4 | std_opts = "-r features/support/ -r features/step_definitions --format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
5 | %>
6 | default: <%= std_opts %> features
7 | wip: --tags @wip:3 --wip features
8 | rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
9 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/spec/controllers/users_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe UsersController do
4 |
5 | before (:each) do
6 | @user = FactoryGirl.create(:user)
7 | sign_in @user
8 | end
9 |
10 | describe "GET 'show'" do
11 |
12 | it "should be successful" do
13 | get :show, :id => @user.id
14 | response.should be_success
15 | end
16 |
17 | it "should find the right user" do
18 | get :show, :id => @user.id
19 | assigns(:user).should == @user
20 | end
21 |
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | RailsRecurlySubscriptionSaas::Application.config.secret_token = '191bf13df5bd1b7f521284b9906784f325d86a8747f7c8e23f9029a0a84114f68f69889f9651b75d71e341fdf0c5917cf8fb463050b14d347e4b2df6c43b04a6'
8 |
--------------------------------------------------------------------------------
/features/step_definitions/form_helper_steps.rb:
--------------------------------------------------------------------------------
1 | When /^I fill in the following:$/ do |fields|
2 | fields.rows_hash.each do |name, value|
3 | step %{I fill in "#{name}" with "#{value}"}
4 | end
5 | end
6 |
7 | When /^I fill in "([^"]*)" with "([^"]*)"$/ do |field, value|
8 | fill_in(field, :with => value)
9 | end
10 |
11 | When /^I select "(.*?)" as the "(.*?)"$/ do |selection, field|
12 | find(:css, "select[id*='card_#{field}']").select(selection)
13 | end
14 |
15 | When /^I press "(.*?)"$/ do |button_name|
16 | click_button(button_name)
17 | end
18 |
--------------------------------------------------------------------------------
/db/migrate/20121221192555_rolify_create_roles.rb:
--------------------------------------------------------------------------------
1 | class RolifyCreateRoles < ActiveRecord::Migration
2 | def change
3 | create_table(:roles) do |t|
4 | t.string :name
5 | t.references :resource, :polymorphic => true
6 |
7 | t.timestamps
8 | end
9 |
10 | create_table(:users_roles, :id => false) do |t|
11 | t.references :user
12 | t.references :role
13 | end
14 |
15 | add_index(:roles, :name)
16 | add_index(:roles, [ :name, :resource_type, :resource_id ])
17 | add_index(:users_roles, [ :user_id, :role_id ])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/mailers/user_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe UserMailer do
4 | describe '#expire_mail' do
5 | let(:user) { FactoryGirl.create(:user) }
6 | let(:mail) { UserMailer.expire_email(user) }
7 |
8 | it "has the correct user email" do
9 | mail.to.should == [user.email]
10 | end
11 |
12 | it "has the correct senders email" do
13 | mail.from.should == ["notifications@example.com"]
14 | end
15 |
16 | it "has the correct subject" do
17 | mail.subject.should == "Subscription Cancelled"
18 | end
19 |
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require_tree .
13 | */
14 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 |
4 | rescue_from CanCan::AccessDenied do |exception|
5 | redirect_to root_path, :alert => exception.message
6 | end
7 |
8 | def after_sign_in_path_for(resource)
9 | case current_user.roles.first.name
10 | when 'admin'
11 | users_path
12 | when 'silver'
13 | content_silver_path
14 | when 'gold'
15 | content_gold_path
16 | when 'platinum'
17 | content_platinum_path
18 | else
19 | root_path
20 | end
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/app/views/layouts/_navigation.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to "Rails Recurly Subscription Saas", root_path, :class => 'brand' %>
2 |
23 |
--------------------------------------------------------------------------------
/spec/recurly/recurly_config_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Config Variables" do
4 |
5 | describe "Recurly.api_key" do
6 |
7 | it "should be set" do
8 | Recurly.api_key.should_not eq("recurly_api_key"),
9 | "Your Recurly.api_key is not set, Please refer to the 'Configure the Recurly Initializer' section of the README"
10 | end
11 |
12 | end
13 |
14 | describe "Recurly.js.private_key" do
15 |
16 | it "should be set" do
17 | Recurly.js.private_key.should_not eq("recurly_js_private_key"),
18 | "Your Recurly.js.private_key is not set, Please refer to the 'Configure the Recurly Initializer' section of the README"
19 | end
20 |
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: sqlite3
8 | database: db/development.sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | # Warning: The database defined as "test" will be erased and
13 | # re-generated from your development database when you run "rake".
14 | # Do not set this db to the same as development or production.
15 | test: &test
16 | adapter: sqlite3
17 | database: db/test.sqlite3
18 | pool: 5
19 | timeout: 5000
20 |
21 | production:
22 | adapter: sqlite3
23 | database: db/production.sqlite3
24 | pool: 5
25 | timeout: 5000
26 |
27 | cucumber:
28 | <<: *test
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // the compiled file.
9 | //
10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11 | // GO AFTER THE REQUIRES BELOW.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require bootstrap
16 | //= require_tree .
17 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
31 |
--------------------------------------------------------------------------------
/features/users/sign_up.feature:
--------------------------------------------------------------------------------
1 | Feature: Sign up
2 | In order to get access to protected sections of the site
3 | As a user
4 | I want to be able to sign up
5 |
6 | Background:
7 | Given I am not logged in
8 |
9 | Scenario: User signs up with invalid email
10 | When I sign up with an invalid email
11 | Then I should see an invalid email message
12 |
13 | Scenario: User signs up without password
14 | When I sign up without a password
15 | Then I should see a missing password message
16 |
17 | Scenario: User signs up without password confirmation
18 | When I sign up without a password confirmation
19 | Then I should see a missing password confirmation message
20 |
21 | Scenario: User signs up with mismatched password and confirmation
22 | When I sign up with a mismatched password confirmation
23 | Then I should see a mismatched password message
24 |
25 | Scenario: User signs up without a subscription plan
26 | When I sign up without a subscription plan
27 | Then I should see a missing subscription plan message
28 |
--------------------------------------------------------------------------------
/features/users/sign_in.feature:
--------------------------------------------------------------------------------
1 | Feature: Sign in
2 | In order to get access to protected sections of the site
3 | A user
4 | Should be able to sign in
5 |
6 | Scenario: User is not signed up
7 | Given I do not exist as a user
8 | When I sign in with valid credentials
9 | Then I see an invalid login message
10 | And I should be signed out
11 |
12 | Scenario: User signs in successfully
13 | Given I exist as a user
14 | And I am not logged in
15 | When I sign in with valid credentials
16 | Then I see a successful sign in message
17 | When I return to the site
18 | Then I should be signed in
19 |
20 | Scenario: User enters wrong email
21 | Given I exist as a user
22 | And I am not logged in
23 | When I sign in with a wrong email
24 | Then I see an invalid login message
25 | And I should be signed out
26 |
27 | Scenario: User enters wrong password
28 | Given I exist as a user
29 | And I am not logged in
30 | When I sign in with a wrong password
31 | Then I see an invalid login message
32 | And I should be signed out
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 |
46 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20121221193007) do
15 |
16 | create_table "roles", :force => true do |t|
17 | t.string "name"
18 | t.integer "resource_id"
19 | t.string "resource_type"
20 | t.datetime "created_at", :null => false
21 | t.datetime "updated_at", :null => false
22 | end
23 |
24 | add_index "roles", ["name", "resource_type", "resource_id"], :name => "index_roles_on_name_and_resource_type_and_resource_id"
25 | add_index "roles", ["name"], :name => "index_roles_on_name"
26 |
27 | create_table "users", :force => true do |t|
28 | t.string "email", :default => "", :null => false
29 | t.string "encrypted_password", :default => "", :null => false
30 | t.string "reset_password_token"
31 | t.datetime "reset_password_sent_at"
32 | t.datetime "remember_created_at"
33 | t.integer "sign_in_count", :default => 0
34 | t.datetime "current_sign_in_at"
35 | t.datetime "last_sign_in_at"
36 | t.string "current_sign_in_ip"
37 | t.string "last_sign_in_ip"
38 | t.datetime "created_at", :null => false
39 | t.datetime "updated_at", :null => false
40 | t.string "first_name"
41 | t.string "last_name"
42 | t.string "customer_id"
43 | end
44 |
45 | add_index "users", ["email"], :name => "index_users_on_email", :unique => true
46 | add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
47 |
48 | create_table "users_roles", :id => false, :force => true do |t|
49 | t.integer "user_id"
50 | t.integer "role_id"
51 | end
52 |
53 | add_index "users_roles", ["user_id", "role_id"], :name => "index_users_roles_on_user_id_and_role_id"
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | rolify
3 | # Include default devise modules. Others available are:
4 | # :token_authenticatable, :confirmable,
5 | # :lockable, :timeoutable and :omniauthable
6 | devise :database_authenticatable, :registerable,
7 | :recoverable, :rememberable, :trackable, :validatable
8 |
9 | # Setup accessible (or protected) attributes for your model
10 | attr_accessible :first_name, :last_name, :email, :password, :password_confirmation, :remember_me, :card_token, :customer_id
11 | attr_accessor :card_token
12 | before_create :check_recurly
13 | before_destroy :cancel_subscription
14 |
15 | def name
16 | name = "#{first_name.capitalize} #{last_name.capitalize}"
17 | end
18 |
19 | def check_recurly
20 | customer = Recurly::Account.find(customer_id) unless customer_id.nil?
21 | rescue Recurly::Resource::NotFound => e
22 | logger.error e.message
23 | errors.add :base, "Unable to create your subscription. #{e.message}"
24 | false
25 | end
26 |
27 | def update_plan(role)
28 | self.role_ids = []
29 | self.add_role(role.name)
30 | customer = Recurly::Account.find(customer_id) unless customer_id.nil?
31 | unless customer.nil?
32 | subscription = customer.subscriptions.first
33 | subscription.update_attributes! :timeframe => 'now', :plan_code => role.name
34 | end
35 | true
36 | rescue Recurly::Resource::Invalid => e
37 | logger.error e.message
38 | errors.add :base, "Unable to update your subscription. #{e.message}"
39 | false
40 | end
41 |
42 | def update_recurly
43 | customer = Recurly::Account.find(customer_id) unless customer_id.nil?
44 | unless customer.nil?
45 | customer.email = email
46 | customer.first_name = first_name
47 | customer.last_name = last_name
48 | customer.save!
49 | end
50 | rescue Recurly::Resource::NotFound => e
51 | logger.error e.message
52 | errors.add :base, "Unable to update your subscription. #{e.message}"
53 | false
54 | end
55 |
56 | def cancel_subscription
57 | unless customer_id.nil?
58 | customer = Recurly::Account.find(customer_id)
59 | subscription = customer.subscriptions.first unless customer.nil?
60 | if (!subscription.nil?) && (subscription.state == 'active')
61 | subscription.cancel
62 | end
63 | end
64 | rescue Recurly::Resource::NotFound => e
65 | logger.error e.message
66 | errors.add :base, "Unable to cancel your subscription. #{e.message}"
67 | false
68 | end
69 |
70 | def expire
71 | UserMailer.expire_email(self).deliver
72 | destroy
73 | end
74 |
75 | end
76 |
--------------------------------------------------------------------------------
/lib/tasks/cucumber.rake:
--------------------------------------------------------------------------------
1 | # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2 | # It is recommended to regenerate this file in the future when you upgrade to a
3 | # newer version of cucumber-rails. Consider adding your own code to a new file
4 | # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5 | # files.
6 |
7 |
8 | unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks
9 |
10 | vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
11 | $LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil?
12 |
13 | begin
14 | require 'cucumber/rake/task'
15 |
16 | namespace :cucumber do
17 | Cucumber::Rake::Task.new({:ok => 'test:prepare'}, 'Run features that should pass') do |t|
18 | t.binary = vendored_cucumber_bin # If nil, the gem's binary is used.
19 | t.fork = true # You may get faster startup if you set this to false
20 | t.profile = 'default'
21 | end
22 |
23 | Cucumber::Rake::Task.new({:wip => 'test:prepare'}, 'Run features that are being worked on') do |t|
24 | t.binary = vendored_cucumber_bin
25 | t.fork = true # You may get faster startup if you set this to false
26 | t.profile = 'wip'
27 | end
28 |
29 | Cucumber::Rake::Task.new({:rerun => 'test:prepare'}, 'Record failing features and run only them if any exist') do |t|
30 | t.binary = vendored_cucumber_bin
31 | t.fork = true # You may get faster startup if you set this to false
32 | t.profile = 'rerun'
33 | end
34 |
35 | desc 'Run all features'
36 | task :all => [:ok, :wip]
37 |
38 | task :statsetup do
39 | require 'rails/code_statistics'
40 | ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features')
41 | ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features')
42 | end
43 | end
44 | desc 'Alias for cucumber:ok'
45 | task :cucumber => 'cucumber:ok'
46 |
47 | task :default => :cucumber
48 |
49 | task :features => :cucumber do
50 | STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***"
51 | end
52 |
53 | # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon.
54 | task 'test:prepare' do
55 | end
56 |
57 | task :stats => 'cucumber:statsetup'
58 | rescue LoadError
59 | desc 'cucumber rake task not available (cucumber not installed)'
60 | task :cucumber do
61 | abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
62 | end
63 | end
64 |
65 | end
66 |
--------------------------------------------------------------------------------
/features/support/env.rb:
--------------------------------------------------------------------------------
1 | # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2 | # It is recommended to regenerate this file in the future when you upgrade to a
3 | # newer version of cucumber-rails. Consider adding your own code to a new file
4 | # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5 | # files.
6 |
7 | require 'cucumber/rails'
8 |
9 | # Capybara defaults to CSS3 selectors rather than XPath.
10 | # If you'd prefer to use XPath, just uncomment this line and adjust any
11 | # selectors in your step definitions to use the XPath syntax.
12 | # Capybara.default_selector = :xpath
13 |
14 | # By default, any exception happening in your Rails application will bubble up
15 | # to Cucumber so that your scenario will fail. This is a different from how
16 | # your application behaves in the production environment, where an error page will
17 | # be rendered instead.
18 | #
19 | # Sometimes we want to override this default behaviour and allow Rails to rescue
20 | # exceptions and display an error page (just like when the app is running in production).
21 | # Typical scenarios where you want to do this is when you test your error pages.
22 | # There are two ways to allow Rails to rescue exceptions:
23 | #
24 | # 1) Tag your scenario (or feature) with @allow-rescue
25 | #
26 | # 2) Set the value below to true. Beware that doing this globally is not
27 | # recommended as it will mask a lot of errors for you!
28 | #
29 | ActionController::Base.allow_rescue = false
30 |
31 | # Remove/comment out the lines below if your app doesn't have a database.
32 | # For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
33 | begin
34 | DatabaseCleaner.strategy = :transaction
35 | rescue NameError
36 | raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
37 | end
38 |
39 | # You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios.
40 | # See the DatabaseCleaner documentation for details. Example:
41 | #
42 | # Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do
43 | # # { :except => [:widgets] } may not do what you expect here
44 | # # as Cucumber::Rails::Database.javascript_strategy overrides
45 | # # this setting.
46 | # DatabaseCleaner.strategy = :truncation
47 | # end
48 | #
49 | # Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do
50 | # DatabaseCleaner.strategy = :transaction
51 | # end
52 | #
53 |
54 | # Possible values are :truncation and :transaction
55 | # The :transaction strategy is faster, but might give you threading problems.
56 | # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature
57 | Cucumber::Rails::Database.javascript_strategy = :truncation
58 |
59 |
--------------------------------------------------------------------------------
/app/controllers/recurly_controller.rb:
--------------------------------------------------------------------------------
1 | class RecurlyController < ApplicationController
2 | protect_from_forgery :except => :push
3 |
4 | def push
5 | notification = Hash.from_xml(request.body())
6 | render :text => "Request accepted."
7 | if notification.has_key?('expired_subscription_notification')
8 | account_code = notification['expired_subscription_notification']['account']['account_code']
9 | logger.info "Recurly push notification: expired_subscription_notification for account #{account_code}"
10 | customer = Recurly::Account.find(account_code)
11 | subscription = customer.subscriptions.first unless customer.nil?
12 | if (!subscription.nil?) && (subscription.state == 'expired')
13 | user = User.find_by_customer_id(account_code)
14 | user.expire unless user.nil?
15 | end
16 | end
17 | rescue Recurly::Resource::NotFound => e
18 | logger.error "Recurly: #{e.message}"
19 | rescue ActiveRecord::RecordNotFound => e
20 | logger.error "Customer record not found: #{e.message}"
21 | end
22 |
23 | def test
24 | xml = <
26 |
27 | 1
28 |
29 | verena@example.com
30 | Verena
31 | Example
32 |
33 |
34 |
35 |
36 | 1dpt
37 | Subscription One
38 |
39 | d1b6d359a01ded71caed78eaa0fedf8e
40 | expired
41 | 1
42 | 200
43 | 2010-09-23T22:05:03Z
44 | 2010-09-23T22:05:43Z
45 | 2010-09-24T22:05:03Z
46 | 2010-09-23T22:05:03Z
47 | 2010-09-24T22:05:03Z
48 |
49 |
50 |
51 |
52 | XML
53 | test_request = HTTPI::Request.new(recurly_push_url)
54 | test_request.open_timeout = 1 # seconds
55 | test_request.read_timeout = 1
56 | test_request.headers = { "Content-Type" => "text/xml" }
57 | test_request.body = xml
58 | test_response = HTTPI.post(test_request)
59 | render :text => "Check server log for result of request to #{recurly_push_url}"
60 | rescue HTTPClient::ReceiveTimeoutError
61 | logger.info "Testing push notification listener: sent XML to #{recurly_push_url}"
62 | render :text => "Check server log for result of request to #{recurly_push_url}"
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | RailsRecurlySubscriptionSaas::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to nil and saved in location specified by config.assets.prefix
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | config.action_mailer.default_url_options = { :host => 'example.com' }
65 | # ActionMailer Config
66 | # Setup for production - deliveries, no errors raised
67 | config.action_mailer.delivery_method = :smtp
68 | config.action_mailer.perform_deliveries = true
69 | config.action_mailer.raise_delivery_errors = false
70 | config.action_mailer.default :charset => "utf-8"
71 |
72 | config.action_mailer.smtp_settings = {
73 | address: "smtp.gmail.com",
74 | port: 587,
75 | domain: "example.com",
76 | authentication: "plain",
77 | enable_starttls_auto: true,
78 | user_name: ENV["GMAIL_USERNAME"],
79 | password: ENV["GMAIL_PASSWORD"]
80 | }
81 |
82 |
83 |
84 | # Log the query plan for queries taking more than this (works
85 | # with SQLite, MySQL, and PostgreSQL)
86 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
87 | end
88 |
--------------------------------------------------------------------------------
/features/users/sign_up_with_recurly.feature:
--------------------------------------------------------------------------------
1 | Feature: User signs up with Recurly
2 | Note: you must have Recurly setup with silver plan
3 | for these tests to run correctly.
4 |
5 | Background:
6 | Given: I am on the home page
7 | When I follow the subscribe for silver path
8 | Then I should see "Silver Subscription Plan"
9 |
10 | @javascript
11 | Scenario: With valid card data
12 | Given I fill in the following:
13 | | user_first_name | Testy |
14 | | user_last_name | McUserson |
15 | | Email | testy@testing.com |
16 | | user_password | secret_password |
17 | | user_password_confirmation | secret_password |
18 | | Credit Card Number | 4111111111111111 |
19 | | card_code | 111 |
20 | Then I select "5 - May" as the "month"
21 | And I select "2015" as the "year"
22 | When I press "Sign up"
23 | Then I should be on the "content silver" page
24 | And I should see a successful sign up message
25 |
26 | @javascript
27 | Scenario: With invalid card number
28 | Given I fill in the following:
29 | | user_first_name | Testy |
30 | | user_last_name | McBadCard |
31 | | Email | testy@testing.com |
32 | | user_password | secret_password |
33 | | user_password_confirmation | secret_password |
34 | | Credit Card Number | 5555555555555 |
35 | | card_code | 111 |
36 | Then I select "1 - January" as the "month"
37 | And I select "2016" as the "year"
38 | When I press "Sign up"
39 | Then I should be on the new silver user registration page
40 | And I should see "Billing info number is not a valid credit card number"
41 |
42 | @javascript
43 | Scenario: With invalid card security code
44 | Given I fill in the following:
45 | | user_first_name | Testy |
46 | | user_last_name | McBadCode |
47 | | Email | testy@testing.com |
48 | | user_password | secret_password |
49 | | user_password_confirmation | secret_password |
50 | | Credit Card Number | 4111111111111111 |
51 | | card_code | 6 |
52 | Then I select "10 - October" as the "month"
53 | And I select "2016" as the "year"
54 | When I press "Sign up"
55 | Then I should be on the new silver user registration page
56 | And I should see "Billing info verification value must be three or four digits"
57 |
58 | @javascript
59 | Scenario: With declined card
60 | Given I fill in the following:
61 | | user_first_name | Testy |
62 | | user_last_name | McDecline |
63 | | Email | testy@testing.com |
64 | | user_password | secret_password |
65 | | user_password_confirmation | secret_password |
66 | | Credit Card Number | 4000000000000002 |
67 | | card_code | 111 |
68 | Then I select "10 - October" as the "month"
69 | And I select "2016" as the "year"
70 | When I press "Sign up"
71 | Then I should be on the new silver user registration page
72 | And I should see "The transaction was declined"
73 |
74 |
75 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # Pick the frameworks you want:
4 | require "active_record/railtie"
5 | require "action_controller/railtie"
6 | require "action_mailer/railtie"
7 | require "active_resource/railtie"
8 | require "sprockets/railtie"
9 | # require "rails/test_unit/railtie"
10 |
11 | if defined?(Bundler)
12 | # If you precompile assets before deploying to production, use this line
13 | Bundler.require(*Rails.groups(:assets => %w(development test)))
14 | # If you want your assets lazily compiled in production, use this line
15 | # Bundler.require(:default, :assets, Rails.env)
16 | end
17 |
18 | module RailsRecurlySubscriptionSaas
19 | class Application < Rails::Application
20 |
21 | # don't generate RSpec tests for views and helpers
22 | config.generators do |g|
23 |
24 | g.test_framework :rspec, fixture: true
25 | g.fixture_replacement :factory_girl, dir: 'spec/factories'
26 |
27 |
28 | g.view_specs false
29 | g.helper_specs false
30 | end
31 |
32 | # Settings in config/environments/* take precedence over those specified here.
33 | # Application configuration should go into files in config/initializers
34 | # -- all .rb files in that directory are automatically loaded.
35 |
36 | # Custom directories with classes and modules you want to be autoloadable.
37 | # config.autoload_paths += %W(#{config.root}/extras)
38 | config.autoload_paths += %W(#{config.root}/lib)
39 |
40 |
41 | # Only load the plugins named here, in the order given (default is alphabetical).
42 | # :all can be used as a placeholder for all plugins not explicitly named.
43 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
44 |
45 | # Activate observers that should always be running.
46 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
47 |
48 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
49 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
50 | # config.time_zone = 'Central Time (US & Canada)'
51 |
52 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
53 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
54 | # config.i18n.default_locale = :de
55 |
56 | # Configure the default encoding used in templates for Ruby 1.9.
57 | config.encoding = "utf-8"
58 |
59 | # Configure sensitive parameters which will be filtered from the log file.
60 | config.filter_parameters += [:password, :password_confirmation]
61 |
62 | # Enable escaping HTML in JSON.
63 | config.active_support.escape_html_entities_in_json = true
64 |
65 | # Use SQL instead of Active Record's schema dumper when creating the database.
66 | # This is necessary if your schema can't be completely dumped by the schema dumper,
67 | # like if you have constraints or database-specific column types
68 | # config.active_record.schema_format = :sql
69 |
70 | # Enforce whitelist mode for mass assignment.
71 | # This will create an empty whitelist of attributes available for mass-assignment for all models
72 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
73 | # parameters by using an attr_accessible or attr_protected declaration.
74 | config.active_record.whitelist_attributes = true
75 |
76 | # Enable the asset pipeline
77 | config.assets.enabled = true
78 |
79 | # Version of your assets, change this if you want to expire all your assets
80 | config.assets.version = '1.0'
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 |
3 | en:
4 | errors:
5 | messages:
6 | expired: "has expired, please request a new one"
7 | not_found: "not found"
8 | already_confirmed: "was already confirmed, please try signing in"
9 | not_locked: "was not locked"
10 | not_saved:
11 | one: "1 error prohibited this %{resource} from being saved:"
12 | other: "%{count} errors prohibited this %{resource} from being saved:"
13 |
14 | devise:
15 | failure:
16 | already_authenticated: 'You are already signed in.'
17 | unauthenticated: 'You need to sign in or sign up before continuing.'
18 | unconfirmed: 'You have to confirm your account before continuing.'
19 | locked: 'Your account is locked.'
20 | invalid: 'Invalid email or password.'
21 | invalid_token: 'Invalid authentication token.'
22 | timeout: 'Your session expired, please sign in again to continue.'
23 | inactive: 'Your account was not activated yet.'
24 | sessions:
25 | signed_in: 'Signed in successfully.'
26 | signed_out: 'Signed out successfully.'
27 | passwords:
28 | send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
29 | updated: 'Your password was changed successfully. You are now signed in.'
30 | updated_not_active: 'Your password was changed successfully.'
31 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
32 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
33 | confirmations:
34 | send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
35 | send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
36 | confirmed: 'Your account was successfully confirmed. You are now signed in.'
37 | registrations:
38 | signed_up: 'Welcome! You have signed up successfully.'
39 | signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
40 | signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
41 | signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
42 | updated: 'You updated your account successfully.'
43 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address."
44 | destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
45 | unlocks:
46 | send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
47 | unlocked: 'Your account has been unlocked successfully. Please sign in to continue.'
48 | send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.'
49 | omniauth_callbacks:
50 | success: 'Successfully authenticated from %{kind} account.'
51 | failure: 'Could not authenticate you from %{kind} because "%{reason}".'
52 | mailer:
53 | confirmation_instructions:
54 | subject: 'Confirmation instructions'
55 | reset_password_instructions:
56 | subject: 'Reset password instructions'
57 | unlock_instructions:
58 | subject: 'Unlock Instructions'
59 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe User do
4 |
5 | before(:each) do
6 | @attr = {
7 | :first_name => "Example",
8 | :last_name => "User",
9 | :email => "user@example.com",
10 | :password => "changeme",
11 | :password_confirmation => "changeme"
12 | }
13 | end
14 |
15 | it "should create a new instance given a valid attribute" do
16 | User.create!(@attr)
17 | end
18 |
19 | it "should require an email address" do
20 | no_email_user = User.new(@attr.merge(:email => ""))
21 | no_email_user.should_not be_valid
22 | end
23 |
24 | it "should accept valid email addresses" do
25 | addresses = %w[user@foo.com THE_USER@foo.bar.org first.last@foo.jp]
26 | addresses.each do |address|
27 | valid_email_user = User.new(@attr.merge(:email => address))
28 | valid_email_user.should be_valid
29 | end
30 | end
31 |
32 | it "should reject invalid email addresses" do
33 | addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
34 | addresses.each do |address|
35 | invalid_email_user = User.new(@attr.merge(:email => address))
36 | invalid_email_user.should_not be_valid
37 | end
38 | end
39 |
40 | it "should reject duplicate email addresses" do
41 | User.create!(@attr)
42 | user_with_duplicate_email = User.new(@attr)
43 | user_with_duplicate_email.should_not be_valid
44 | end
45 |
46 | it "should reject email addresses identical up to case" do
47 | upcased_email = @attr[:email].upcase
48 | User.create!(@attr.merge(:email => upcased_email))
49 | user_with_duplicate_email = User.new(@attr)
50 | user_with_duplicate_email.should_not be_valid
51 | end
52 |
53 | describe "passwords" do
54 |
55 | before(:each) do
56 | @user = User.new(@attr)
57 | end
58 |
59 | it "should have a password attribute" do
60 | @user.should respond_to(:password)
61 | end
62 |
63 | it "should have a password confirmation attribute" do
64 | @user.should respond_to(:password_confirmation)
65 | end
66 | end
67 |
68 | describe "password validations" do
69 |
70 | it "should require a password" do
71 | User.new(@attr.merge(:password => "", :password_confirmation => "")).
72 | should_not be_valid
73 | end
74 |
75 | it "should require a matching password confirmation" do
76 | User.new(@attr.merge(:password_confirmation => "invalid")).
77 | should_not be_valid
78 | end
79 |
80 | it "should reject short passwords" do
81 | short = "a" * 5
82 | hash = @attr.merge(:password => short, :password_confirmation => short)
83 | User.new(hash).should_not be_valid
84 | end
85 |
86 | end
87 |
88 | describe "password encryption" do
89 |
90 | before(:each) do
91 | @user = User.create!(@attr)
92 | end
93 |
94 | it "should have an encrypted password attribute" do
95 | @user.should respond_to(:encrypted_password)
96 | end
97 |
98 | it "should set the encrypted password attribute" do
99 | @user.encrypted_password.should_not be_blank
100 | end
101 |
102 | end
103 |
104 | describe "expire" do
105 |
106 | before(:each) do
107 | @user = User.create!(@attr)
108 | end
109 |
110 | it "sends an email to user" do
111 | @user.expire
112 | ActionMailer::Base.deliveries.last.to.should == [@user.email]
113 | end
114 |
115 | end
116 |
117 | describe "#update_plan" do
118 | before do
119 | @user = FactoryGirl.create(:user, email: "test@example.com")
120 | @role1 = FactoryGirl.create(:role, name: "silver")
121 | @role2 = FactoryGirl.create(:role, name: "gold")
122 | @user.add_role(@role1.name)
123 | end
124 |
125 | it "updates a users role" do
126 | @user.roles.first.name.should == "silver"
127 | @user.update_plan(@role2)
128 | @user.roles.first.name.should == "gold"
129 | end
130 |
131 | it "wont remove original role from database" do
132 | @user.update_plan(@role2)
133 | Role.all.count.should == 2
134 | end
135 | end
136 |
137 | end
138 |
--------------------------------------------------------------------------------
/config/initializers/simple_form.rb:
--------------------------------------------------------------------------------
1 | # Use this setup block to configure all options available in SimpleForm.
2 | SimpleForm.setup do |config|
3 | # Wrappers are used by the form builder to generate a
4 | # complete input. You can remove any component from the
5 | # wrapper, change the order or even add your own to the
6 | # stack. The options given below are used to wrap the
7 | # whole input.
8 | config.wrappers :default, :class => :input,
9 | :hint_class => :field_with_hint, :error_class => :field_with_errors do |b|
10 | ## Extensions enabled by default
11 | # Any of these extensions can be disabled for a
12 | # given input by passing: `f.input EXTENSION_NAME => false`.
13 | # You can make any of these extensions optional by
14 | # renaming `b.use` to `b.optional`.
15 |
16 | # Determines whether to use HTML5 (:email, :url, ...)
17 | # and required attributes
18 | b.use :html5
19 |
20 | # Calculates placeholders automatically from I18n
21 | # You can also pass a string as f.input :placeholder => "Placeholder"
22 | b.use :placeholder
23 |
24 | ## Optional extensions
25 | # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup`
26 | # to the input. If so, they will retrieve the values from the model
27 | # if any exists. If you want to enable the lookup for any of those
28 | # extensions by default, you can change `b.optional` to `b.use`.
29 |
30 | # Calculates maxlength from length validations for string inputs
31 | b.optional :maxlength
32 |
33 | # Calculates pattern from format validations for string inputs
34 | b.optional :pattern
35 |
36 | # Calculates min and max from length validations for numeric inputs
37 | b.optional :min_max
38 |
39 | # Calculates readonly automatically from readonly attributes
40 | b.optional :readonly
41 |
42 | ## Inputs
43 | b.use :label_input
44 | b.use :hint, :wrap_with => { :tag => :span, :class => :hint }
45 | b.use :error, :wrap_with => { :tag => :span, :class => :error }
46 | end
47 |
48 | # The default wrapper to be used by the FormBuilder.
49 | config.default_wrapper = :default
50 |
51 | # Define the way to render check boxes / radio buttons with labels.
52 | # Defaults to :nested for bootstrap config.
53 | # :inline => input + label
54 | # :nested => label > input
55 | config.boolean_style = :nested
56 |
57 | # Default class for buttons
58 | config.button_class = 'btn'
59 |
60 | # Method used to tidy up errors. Specify any Rails Array method.
61 | # :first lists the first message for each field.
62 | # Use :to_sentence to list all errors for each field.
63 | # config.error_method = :first
64 |
65 | # Default tag used for error notification helper.
66 | config.error_notification_tag = :div
67 |
68 | # CSS class to add for error notification helper.
69 | config.error_notification_class = 'alert alert-error'
70 |
71 | # ID to add for error notification helper.
72 | # config.error_notification_id = nil
73 |
74 | # Series of attempts to detect a default label method for collection.
75 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
76 |
77 | # Series of attempts to detect a default value method for collection.
78 | # config.collection_value_methods = [ :id, :to_s ]
79 |
80 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
81 | # config.collection_wrapper_tag = nil
82 |
83 | # You can define the class to use on all collection wrappers. Defaulting to none.
84 | # config.collection_wrapper_class = nil
85 |
86 | # You can wrap each item in a collection of radio/check boxes with a tag,
87 | # defaulting to :span. Please note that when using :boolean_style = :nested,
88 | # SimpleForm will force this option to be a label.
89 | # config.item_wrapper_tag = :span
90 |
91 | # You can define a class to use in all item wrappers. Defaulting to none.
92 | # config.item_wrapper_class = nil
93 |
94 | # How the label text should be generated altogether with the required text.
95 | # config.label_text = lambda { |label, required| "#{required} #{label}" }
96 |
97 | # You can define the class to use on all labels. Default is nil.
98 | config.label_class = 'control-label'
99 |
100 | # You can define the class to use on all forms. Default is simple_form.
101 | # config.form_class = :simple_form
102 |
103 | # You can define which elements should obtain additional classes
104 | # config.generate_additional_classes_for = [:wrapper, :label, :input]
105 |
106 | # Whether attributes are required by default (or not). Default is true.
107 | # config.required_by_default = true
108 |
109 | # Tell browsers whether to use default HTML5 validations (novalidate option).
110 | # Default is enabled.
111 | config.browser_validations = false
112 |
113 | # Collection of methods to detect if a file type was given.
114 | # config.file_methods = [ :mounted_as, :file?, :public_filename ]
115 |
116 | # Custom mappings for input types. This should be a hash containing a regexp
117 | # to match as key, and the input type that will be used when the field name
118 | # matches the regexp as value.
119 | # config.input_mappings = { /count/ => :integer }
120 |
121 | # Custom wrappers for input types. This should be a hash containing an input
122 | # type as key and the wrapper that will be used for all inputs with specified type.
123 | # config.wrapper_mappings = { :string => :prepend }
124 |
125 | # Default priority for time_zone inputs.
126 | # config.time_zone_priority = nil
127 |
128 | # Default priority for country inputs.
129 | # config.country_priority = nil
130 |
131 | # Default size for text inputs.
132 | # config.default_input_size = 50
133 |
134 | # When false, do not use translations for labels.
135 | # config.translate_labels = true
136 |
137 | # Automatically discover new inputs in Rails' autoload path.
138 | # config.inputs_discovery = true
139 |
140 | # Cache SimpleForm inputs discovery
141 | # config.cache_discovery = !Rails.env.development?
142 | end
143 |
--------------------------------------------------------------------------------
/app/assets/javascripts/recurly.js:
--------------------------------------------------------------------------------
1 | // Recurly.js
2 | // JavaScript library for the Recurly API
3 | // adapted for the rails-recurly-subscription-saas example application
4 | // from https://github.com/recurly/recurly-js
5 |
6 | function createObject(o) {
7 | function F() {}
8 | F.prototype = o || this;
9 | return new F();
10 | };
11 |
12 | var Recurly = {};
13 |
14 | $('.registrations.new').ready(function() {
15 |
16 | Recurly.settings = {
17 | enableGeoIP: true
18 | , acceptedCards: ['american_express', 'discover', 'mastercard', 'visa']
19 | , oneErrorPerField: true
20 | , baseURL: 'https://api.recurly.com/jsonp/' + $('#new_user').data('subdomain') + '/'
21 | };
22 |
23 | Recurly.version = '2.1.8';
24 |
25 | Recurly.ajax = function(options) {
26 | options.data = $.extend({js_version: Recurly.version}, options.data);
27 | return $.ajax(options);
28 | };
29 |
30 | Recurly.flattenErrors = function(obj, attr) {
31 | var arr = [];
32 | var attr = attr || '';
33 | if( typeof obj == 'string'
34 | || typeof obj == 'number'
35 | || typeof obj == 'boolean') {
36 | if (attr == 'base') {
37 | return [obj];
38 | }
39 | return ['' + attr + ' ' + obj];
40 | }
41 | for(var k in obj) {
42 | if(obj.hasOwnProperty(k)) {
43 | // Inherit parent attribute names when property key
44 | // is a numeric string; how we deal with arrays
45 | attr = (parseInt(k).toString() == k) ? attr : k;
46 | var children = Recurly.flattenErrors(obj[k], attr);
47 | for(var i=0, l=children.length; i < l; ++i) {
48 | arr.push(children[i]);
49 | }
50 | }
51 | }
52 | return arr;
53 | };
54 |
55 | Recurly.Account = {
56 | create: createObject
57 | , toJSON: function(card) {
58 | return {
59 | first_name: card.first_name
60 | , last_name: card.last_name
61 | , account_code: card.customer_id
62 | , email: card.email
63 | };
64 | }
65 | };
66 |
67 | Recurly.BillingInfo = {
68 | create: createObject
69 | , toJSON: function(card) {
70 | return {
71 | first_name: card.first_name
72 | , last_name: card.last_name
73 | , month: card.expMonth
74 | , year: card.expYear
75 | , number: card.number
76 | , verification_value: card.cvc
77 | , country: card.country
78 | , ip_address: card.ip_address
79 | };
80 | }
81 | };
82 |
83 | Recurly.Subscription = {
84 | create: createObject
85 | , plan: Recurly.Plan
86 | , addOns: []
87 |
88 | , calculateTotals: function() {
89 | var totals = {
90 | stages: {}
91 | };
92 |
93 | // PLAN
94 | totals.plan = this.plan.cost.mult(this.plan.quantity);
95 |
96 | // ADD-ONS
97 | totals.allAddOns = new Recurly.Cost(0);
98 | totals.addOns = {};
99 | for(var l=this.addOns.length, i=0; i < l; ++i) {
100 | var a = this.addOns[i],
101 | c = a.cost.mult(a.quantity);
102 | totals.addOns[a.code] = c;
103 | totals.allAddOns = totals.allAddOns.add(c);
104 | }
105 |
106 | totals.stages.recurring = totals.plan.add(totals.allAddOns);
107 |
108 | totals.stages.now = totals.plan.add(totals.allAddOns);
109 |
110 | // FREE TRIAL
111 | if(this.plan.trial) {
112 | totals.stages.now = Recurly.Cost.FREE;
113 | }
114 |
115 | // COUPON
116 | if(this.coupon) {
117 | var beforeDiscount = totals.stages.now;
118 | var afterDiscount = totals.stages.now.discount(this.coupon);
119 | totals.coupon = afterDiscount.sub(beforeDiscount);
120 | totals.stages.now = afterDiscount;
121 | }
122 |
123 | // SETUP FEE
124 | if(this.plan.setupFee) {
125 | totals.stages.now = totals.stages.now.add(this.plan.setupFee);
126 | }
127 |
128 | // VAT
129 | if(this.billingInfo && Recurly.isVATChargeApplicable(this.billingInfo.country,this.billingInfo.vatNumber)) {
130 | totals.vat = totals.stages.now.mult( (Recurly.settings.VATPercent/100) );
131 | totals.stages.now = totals.stages.now.add(totals.vat);
132 | }
133 |
134 | return totals;
135 | }
136 | , redeemAddOn: function(addOn) {
137 | var redemption = addOn.createRedemption();
138 | this.addOns.push(redemption);
139 | return redemption;
140 | }
141 |
142 | , removeAddOn: function(code) {
143 | for(var a=this.addOns, l=a.length, i=0; i < l; ++i) {
144 | if(a[i].code == code) {
145 | return a.splice(i,1);
146 | }
147 | }
148 | }
149 |
150 | , findAddOnByCode: function(code) {
151 | for(var l=this.addOns.length, i=0; i < l; ++i) {
152 | if(this.addOns[i].code == code) {
153 | return this.addOns[i];
154 | }
155 | }
156 | return false;
157 | }
158 |
159 | , toJSON: function(plan, coupon) {
160 | var json = {
161 | plan_code: plan.plan_code
162 | , quantity: plan.quantity ? plan.quantity : 1
163 | , currency: plan.currency ? plan.quantity : 'USD'
164 | , coupon_code: coupon.coupon_code ? coupon.coupon_code : undefined
165 | , add_ons: []
166 | };
167 |
168 | for(var i=0, l=this.addOns.length, a=json.add_ons, b=this.addOns; i < l; ++i) {
169 | a.push({
170 | add_on_code: b[i].code
171 | , quantity: b[i].quantity
172 | });
173 | }
174 |
175 | return json;
176 | }
177 |
178 | , save: function(signature, plan, coupon, card, callback) {
179 | var json = {
180 | subscription: this.toJSON(plan, coupon)
181 | , account: Recurly.Account.toJSON(card)
182 | , billing_info: Recurly.BillingInfo.toJSON(card)
183 | , signature: signature
184 | };
185 |
186 | Recurly.ajax({
187 | url: Recurly.settings.baseURL+'subscribe',
188 | data: json,
189 | dataType: "jsonp",
190 | jsonp: "callback",
191 | timeout: 60000,
192 | success: function(response){
193 | callback(response)
194 | },
195 | error: function() {
196 | console.log(['Unknown error processing transaction. Please try again later.']);
197 | }
198 | });
199 |
200 | }
201 | };
202 | });
203 |
--------------------------------------------------------------------------------
/features/step_definitions/user_steps.rb:
--------------------------------------------------------------------------------
1 | ### UTILITY METHODS ###
2 |
3 | def create_visitor
4 | @visitor ||= { :name => "Testy McUserton", :email => "example@example.com",
5 | :password => "changeme", :password_confirmation => "changeme", :role => "silver" }
6 | end
7 |
8 | def find_user
9 | @user ||= User.first conditions: {:email => @visitor[:email]}
10 | end
11 |
12 | def create_unconfirmed_user
13 | create_visitor
14 | delete_user
15 | sign_up
16 | visit '/users/sign_out'
17 | end
18 |
19 | def create_user
20 | create_visitor
21 | delete_user
22 | @user = FactoryGirl.create(:user, email: @visitor[:email])
23 | @user.add_role(@visitor[:role])
24 | end
25 |
26 | def delete_user
27 | @user ||= User.first conditions: {:email => @visitor[:email]}
28 | @user.destroy unless @user.nil?
29 | end
30 |
31 | def sign_up
32 | delete_user
33 | visit '/users/sign_up/?plan=silver'
34 | fill_in "user_first_name", :with => @visitor[:first_name]
35 | fill_in "user_last_name", :with => @visitor[:last_name]
36 | fill_in "user_email", :with => @visitor[:email]
37 | fill_in "user_password", :with => @visitor[:password]
38 | fill_in "user_password_confirmation", :with => @visitor[:password_confirmation]
39 | click_button "Sign up"
40 | find_user
41 | end
42 |
43 | def sign_in
44 | visit '/users/sign_in'
45 | fill_in "Email", :with => @visitor[:email]
46 | fill_in "Password", :with => @visitor[:password]
47 | click_button "Sign in"
48 | end
49 |
50 | ### GIVEN ###
51 | Given /^I am not logged in$/ do
52 | visit destroy_user_session_path
53 | end
54 |
55 | Given /^I am logged in$/ do
56 | create_user
57 | sign_in
58 | end
59 |
60 | Given /^I exist as a user$/ do
61 | create_user
62 | end
63 |
64 | Given /^I do not exist as a user$/ do
65 | create_visitor
66 | delete_user
67 | end
68 |
69 | Given /^I exist as an unconfirmed user$/ do
70 | create_unconfirmed_user
71 | end
72 |
73 | ### WHEN ###
74 | When /^I sign in with valid credentials$/ do
75 | create_visitor
76 | sign_in
77 | end
78 |
79 | When /^I sign out$/ do
80 | visit '/users/sign_out'
81 | end
82 |
83 | When /^I sign up with valid user data$/ do
84 | create_visitor
85 | sign_up
86 | end
87 |
88 | When /^I sign up with an invalid email$/ do
89 | create_visitor
90 | @visitor = @visitor.merge(:email => "notanemail")
91 | sign_up
92 | end
93 |
94 | When /^I sign up without a password confirmation$/ do
95 | create_visitor
96 | @visitor = @visitor.merge(:password_confirmation => "")
97 | sign_up
98 | end
99 |
100 | When /^I sign up without a password$/ do
101 | create_visitor
102 | @visitor = @visitor.merge(:password => "")
103 | sign_up
104 | end
105 |
106 | When /^I sign up without a subscription plan$/ do
107 | visit '/users/sign_up'
108 | end
109 |
110 | When /^I sign up with a mismatched password confirmation$/ do
111 | create_visitor
112 | @visitor = @visitor.merge(:password_confirmation => "changeme123")
113 | sign_up
114 | end
115 |
116 | When /^I return to the site$/ do
117 | visit '/'
118 | end
119 |
120 | When /^I sign in with a wrong email$/ do
121 | @visitor = @visitor.merge(:email => "wrong@example.com")
122 | sign_in
123 | end
124 |
125 | When /^I sign in with a wrong password$/ do
126 | @visitor = @visitor.merge(:password => "wrongpass")
127 | sign_in
128 | end
129 |
130 | When /^I change my email address$/ do
131 | click_link "Edit account"
132 | fill_in "user_email", :with => "different@example.com"
133 | fill_in "user_current_password", :with => @visitor[:password]
134 | click_button "Update"
135 | end
136 |
137 | When /^I delete my account$/ do
138 | click_link "Edit account"
139 | click_link "Cancel my account"
140 | page.driver.browser.switch_to.alert.accept
141 | end
142 |
143 | When /^I follow the subscribe for silver path$/ do
144 | visit '/users/sign_up/?plan=silver'
145 | end
146 |
147 | ### THEN ###
148 | Then /^I should be signed in$/ do
149 | page.should have_content "Logout"
150 | page.should_not have_content "Sign up"
151 | page.should_not have_content "Login"
152 | end
153 |
154 | Then /^I should be signed out$/ do
155 | page.should have_content "Login"
156 | page.should_not have_content "Logout"
157 | end
158 |
159 | Then /^I should see "(.*?)"$/ do |text|
160 | page.should have_content text
161 | end
162 |
163 | Then /^I should be on the "([^"]*)" page$/ do |path_name|
164 | current_path.should == send("#{path_name.parameterize('_')}_path")
165 | end
166 |
167 | Then /I should be on the new silver user registration page$/ do
168 | current_path_with_args.should == '/users/sign_up/?plan=silver'
169 | end
170 |
171 | Then /^I see an unconfirmed account message$/ do
172 | page.should have_content "You have to confirm your account before continuing."
173 | end
174 |
175 | Then /^I see a successful sign in message$/ do
176 | page.should have_content "Signed in successfully."
177 | end
178 |
179 | Then /^I should see a successful sign up message$/ do
180 | page.should have_content "Welcome! You have signed up successfully."
181 | end
182 |
183 | Then /^I should see an invalid email message$/ do
184 | page.should have_content "is invalid"
185 | end
186 |
187 | Then /^I should see a missing password message$/ do
188 | page.should have_content "can't be blank"
189 | end
190 |
191 | Then /^I should see a missing password confirmation message$/ do
192 | page.should have_content "doesn't match confirmation"
193 | end
194 |
195 | Then /^I should see a mismatched password message$/ do
196 | page.should have_content "doesn't match confirmation"
197 | end
198 |
199 | Then /^I should see a missing subscription plan message$/ do
200 | page.should have_content "Please select a subscription plan below"
201 | end
202 |
203 | Then /^I should see a signed out message$/ do
204 | page.should have_content "Signed out successfully."
205 | end
206 |
207 | Then /^I see an invalid login message$/ do
208 | page.should have_content "Invalid email or password."
209 | end
210 |
211 | Then /^I should see an account edited message$/ do
212 | page.should have_content "You updated your account successfully."
213 | end
214 |
215 | Then /^I should see an account deleted message$/ do
216 | page.should have_content "account was successfully cancelled"
217 | end
218 |
219 | Then /^I should see my name$/ do
220 | create_user
221 | page.should have_content @user[:name]
222 | end
223 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (3.2.13)
5 | actionpack (= 3.2.13)
6 | mail (~> 2.5.3)
7 | actionpack (3.2.13)
8 | activemodel (= 3.2.13)
9 | activesupport (= 3.2.13)
10 | builder (~> 3.0.0)
11 | erubis (~> 2.7.0)
12 | journey (~> 1.0.4)
13 | rack (~> 1.4.5)
14 | rack-cache (~> 1.2)
15 | rack-test (~> 0.6.1)
16 | sprockets (~> 2.2.1)
17 | activemodel (3.2.13)
18 | activesupport (= 3.2.13)
19 | builder (~> 3.0.0)
20 | activerecord (3.2.13)
21 | activemodel (= 3.2.13)
22 | activesupport (= 3.2.13)
23 | arel (~> 3.0.2)
24 | tzinfo (~> 0.3.29)
25 | activeresource (3.2.13)
26 | activemodel (= 3.2.13)
27 | activesupport (= 3.2.13)
28 | activesupport (3.2.13)
29 | i18n (= 0.6.1)
30 | multi_json (~> 1.0)
31 | addressable (2.3.3)
32 | arel (3.0.2)
33 | bcrypt-ruby (3.0.1)
34 | better_errors (0.8.0)
35 | coderay (>= 1.0.0)
36 | erubis (>= 2.6.6)
37 | binding_of_caller (0.7.1)
38 | debug_inspector (>= 0.0.1)
39 | bootstrap-sass (2.3.1.0)
40 | sass (~> 3.2)
41 | builder (3.0.4)
42 | cancan (1.6.9)
43 | capybara (2.0.3)
44 | mime-types (>= 1.16)
45 | nokogiri (>= 1.3.3)
46 | rack (>= 1.0.0)
47 | rack-test (>= 0.5.4)
48 | selenium-webdriver (~> 2.0)
49 | xpath (~> 1.0.0)
50 | childprocess (0.3.9)
51 | ffi (~> 1.0, >= 1.0.11)
52 | coderay (1.0.9)
53 | coffee-rails (3.2.2)
54 | coffee-script (>= 2.2.0)
55 | railties (~> 3.2.0)
56 | coffee-script (2.2.0)
57 | coffee-script-source
58 | execjs
59 | coffee-script-source (1.6.2)
60 | countries (0.9.2)
61 | currencies (>= 0.4.0)
62 | cucumber (1.2.3)
63 | builder (>= 2.1.2)
64 | diff-lcs (>= 1.1.3)
65 | gherkin (~> 2.11.6)
66 | multi_json (~> 1.3)
67 | cucumber-rails (1.3.1)
68 | capybara (>= 1.1.2)
69 | cucumber (>= 1.2.0)
70 | nokogiri (>= 1.5.0)
71 | rails (~> 3.0)
72 | currencies (0.4.0)
73 | database_cleaner (1.0.0.RC1)
74 | debug_inspector (0.0.2)
75 | devise (2.2.3)
76 | bcrypt-ruby (~> 3.0)
77 | orm_adapter (~> 0.1)
78 | railties (~> 3.1)
79 | warden (~> 1.2.1)
80 | diff-lcs (1.2.2)
81 | email_spec (1.4.0)
82 | launchy (~> 2.1)
83 | mail (~> 2.2)
84 | erubis (2.7.0)
85 | execjs (1.4.0)
86 | multi_json (~> 1.0)
87 | factory_girl (4.2.0)
88 | activesupport (>= 3.0.0)
89 | factory_girl_rails (4.2.1)
90 | factory_girl (~> 4.2.0)
91 | railties (>= 3.0.0)
92 | ffi (1.6.0)
93 | figaro (0.6.3)
94 | bundler (~> 1.0)
95 | rails (>= 3, < 5)
96 | gherkin (2.11.6)
97 | json (>= 1.7.6)
98 | hike (1.2.1)
99 | httpclient (2.3.3)
100 | httpi (2.0.2)
101 | rack
102 | i18n (0.6.1)
103 | journey (1.0.4)
104 | jquery-rails (2.2.1)
105 | railties (>= 3.0, < 5.0)
106 | thor (>= 0.14, < 2.0)
107 | json (1.7.7)
108 | launchy (2.2.0)
109 | addressable (~> 2.3)
110 | mail (2.5.3)
111 | i18n (>= 0.4.0)
112 | mime-types (~> 1.16)
113 | treetop (~> 1.4.8)
114 | mime-types (1.22)
115 | multi_json (1.7.2)
116 | nokogiri (1.5.9)
117 | orm_adapter (0.4.0)
118 | polyglot (0.3.3)
119 | quiet_assets (1.0.2)
120 | railties (>= 3.1, < 5.0)
121 | rack (1.4.5)
122 | rack-cache (1.2)
123 | rack (>= 0.4)
124 | rack-ssl (1.3.3)
125 | rack
126 | rack-test (0.6.2)
127 | rack (>= 1.0)
128 | rails (3.2.13)
129 | actionmailer (= 3.2.13)
130 | actionpack (= 3.2.13)
131 | activerecord (= 3.2.13)
132 | activeresource (= 3.2.13)
133 | activesupport (= 3.2.13)
134 | bundler (~> 1.0)
135 | railties (= 3.2.13)
136 | railties (3.2.13)
137 | actionpack (= 3.2.13)
138 | activesupport (= 3.2.13)
139 | rack-ssl (~> 1.3.2)
140 | rake (>= 0.8.7)
141 | rdoc (~> 3.4)
142 | thor (>= 0.14.6, < 2.0)
143 | rake (10.0.4)
144 | rdoc (3.12.2)
145 | json (~> 1.4)
146 | recurly (2.1.8)
147 | rolify (3.2.0)
148 | rspec-core (2.13.1)
149 | rspec-expectations (2.13.0)
150 | diff-lcs (>= 1.1.3, < 2.0)
151 | rspec-mocks (2.13.0)
152 | rspec-rails (2.13.0)
153 | actionpack (>= 3.0)
154 | activesupport (>= 3.0)
155 | railties (>= 3.0)
156 | rspec-core (~> 2.13.0)
157 | rspec-expectations (~> 2.13.0)
158 | rspec-mocks (~> 2.13.0)
159 | rubyzip (0.9.9)
160 | sass (3.2.7)
161 | sass-rails (3.2.6)
162 | railties (~> 3.2.0)
163 | sass (>= 3.1.10)
164 | tilt (~> 1.3)
165 | selenium-webdriver (2.31.0)
166 | childprocess (>= 0.2.5)
167 | multi_json (~> 1.0)
168 | rubyzip
169 | websocket (~> 1.0.4)
170 | simple_form (2.1.0)
171 | actionpack (~> 3.0)
172 | activemodel (~> 3.0)
173 | sprockets (2.2.2)
174 | hike (~> 1.2)
175 | multi_json (~> 1.0)
176 | rack (~> 1.0)
177 | tilt (~> 1.1, != 1.3.0)
178 | sqlite3 (1.3.7)
179 | thor (0.18.1)
180 | tilt (1.3.6)
181 | treetop (1.4.12)
182 | polyglot
183 | polyglot (>= 0.3.1)
184 | tzinfo (0.3.37)
185 | uglifier (1.3.0)
186 | execjs (>= 0.3.0)
187 | multi_json (~> 1.0, >= 1.0.2)
188 | warden (1.2.1)
189 | rack (>= 1.0)
190 | websocket (1.0.7)
191 | xpath (1.0.0)
192 | nokogiri (~> 1.3)
193 |
194 | PLATFORMS
195 | ruby
196 |
197 | DEPENDENCIES
198 | better_errors (>= 0.7.2)
199 | binding_of_caller (>= 0.7.1)
200 | bootstrap-sass (>= 2.3.0.0)
201 | cancan (>= 1.6.9)
202 | capybara (>= 2.0.3)
203 | coffee-rails (~> 3.2.1)
204 | countries (>= 0.9.2)
205 | cucumber-rails (>= 1.3.1)
206 | database_cleaner (>= 1.0.0.RC1)
207 | devise (>= 2.2.3)
208 | email_spec (>= 1.4.0)
209 | factory_girl_rails (>= 4.2.0)
210 | figaro (>= 0.6.3)
211 | httpclient (>= 2.3.3)
212 | httpi (>= 1.1.1)
213 | jquery-rails
214 | launchy (>= 2.2.0)
215 | nokogiri (>= 1.5.5)
216 | quiet_assets (>= 1.0.2)
217 | rails (= 3.2.13)
218 | recurly (>= 2.1.8)
219 | rolify (>= 3.2.0)
220 | rspec-rails (>= 2.12.2)
221 | sass-rails (~> 3.2.3)
222 | simple_form (>= 2.1.0)
223 | sqlite3
224 | uglifier (>= 1.0.3)
225 |
--------------------------------------------------------------------------------
/features/step_definitions/email_steps.rb:
--------------------------------------------------------------------------------
1 | # Commonly used email steps
2 | #
3 | # To add your own steps make a custom_email_steps.rb
4 | # The provided methods are:
5 | #
6 | # last_email_address
7 | # reset_mailer
8 | # open_last_email
9 | # visit_in_email
10 | # unread_emails_for
11 | # mailbox_for
12 | # current_email
13 | # open_email
14 | # read_emails_for
15 | # find_email
16 | #
17 | # General form for email scenarios are:
18 | # - clear the email queue (done automatically by email_spec)
19 | # - execute steps that sends an email
20 | # - check the user received an/no/[0-9] emails
21 | # - open the email
22 | # - inspect the email contents
23 | # - interact with the email (e.g. click links)
24 | #
25 | # The Cucumber steps below are setup in this order.
26 |
27 | module EmailHelpers
28 | def current_email_address
29 | # Replace with your a way to find your current email. e.g @current_user.email
30 | # last_email_address will return the last email address used by email spec to find an email.
31 | # Note that last_email_address will be reset after each Scenario.
32 | last_email_address || "example@example.com"
33 | end
34 | end
35 |
36 | World(EmailHelpers)
37 |
38 | #
39 | # Reset the e-mail queue within a scenario.
40 | # This is done automatically before each scenario.
41 | #
42 |
43 | Given /^(?:a clear email queue|no emails have been sent)$/ do
44 | reset_mailer
45 | end
46 |
47 | #
48 | # Check how many emails have been sent/received
49 | #
50 |
51 | Then /^(?:I|they|"([^"]*?)") should receive (an|no|\d+) emails?$/ do |address, amount|
52 | unread_emails_for(address).size.should == parse_email_count(amount)
53 | end
54 |
55 | Then /^(?:I|they|"([^"]*?)") should have (an|no|\d+) emails?$/ do |address, amount|
56 | mailbox_for(address).size.should == parse_email_count(amount)
57 | end
58 |
59 | Then /^(?:I|they|"([^"]*?)") should receive (an|no|\d+) emails? with subject "([^"]*?)"$/ do |address, amount, subject|
60 | unread_emails_for(address).select { |m| m.subject =~ Regexp.new(Regexp.escape(subject)) }.size.should == parse_email_count(amount)
61 | end
62 |
63 | Then /^(?:I|they|"([^"]*?)") should receive (an|no|\d+) emails? with subject \/([^"]*?)\/$/ do |address, amount, subject|
64 | unread_emails_for(address).select { |m| m.subject =~ Regexp.new(subject) }.size.should == parse_email_count(amount)
65 | end
66 |
67 | Then /^(?:I|they|"([^"]*?)") should receive an email with the following body:$/ do |address, expected_body|
68 | open_email(address, :with_text => expected_body)
69 | end
70 |
71 | #
72 | # Accessing emails
73 | #
74 |
75 | # Opens the most recently received email
76 | When /^(?:I|they|"([^"]*?)") opens? the email$/ do |address|
77 | open_email(address)
78 | end
79 |
80 | When /^(?:I|they|"([^"]*?)") opens? the email with subject "([^"]*?)"$/ do |address, subject|
81 | open_email(address, :with_subject => subject)
82 | end
83 |
84 | When /^(?:I|they|"([^"]*?)") opens? the email with subject \/([^"]*?)\/$/ do |address, subject|
85 | open_email(address, :with_subject => Regexp.new(subject))
86 | end
87 |
88 | When /^(?:I|they|"([^"]*?)") opens? the email with text "([^"]*?)"$/ do |address, text|
89 | open_email(address, :with_text => text)
90 | end
91 |
92 | When /^(?:I|they|"([^"]*?)") opens? the email with text \/([^"]*?)\/$/ do |address, text|
93 | open_email(address, :with_text => Regexp.new(text))
94 | end
95 |
96 | #
97 | # Inspect the Email Contents
98 | #
99 |
100 | Then /^(?:I|they) should see "([^"]*?)" in the email subject$/ do |text|
101 | current_email.should have_subject(text)
102 | end
103 |
104 | Then /^(?:I|they) should see \/([^"]*?)\/ in the email subject$/ do |text|
105 | current_email.should have_subject(Regexp.new(text))
106 | end
107 |
108 | Then /^(?:I|they) should see "([^"]*?)" in the email body$/ do |text|
109 | current_email.default_part_body.to_s.should include(text)
110 | end
111 |
112 | Then /^(?:I|they) should see \/([^"]*?)\/ in the email body$/ do |text|
113 | current_email.default_part_body.to_s.should =~ Regexp.new(text)
114 | end
115 |
116 | Then /^(?:I|they) should see the email delivered from "([^"]*?)"$/ do |text|
117 | current_email.should be_delivered_from(text)
118 | end
119 |
120 | Then /^(?:I|they) should see "([^\"]*)" in the email "([^"]*?)" header$/ do |text, name|
121 | current_email.should have_header(name, text)
122 | end
123 |
124 | Then /^(?:I|they) should see \/([^\"]*)\/ in the email "([^"]*?)" header$/ do |text, name|
125 | current_email.should have_header(name, Regexp.new(text))
126 | end
127 |
128 | Then /^I should see it is a multi\-part email$/ do
129 | current_email.should be_multipart
130 | end
131 |
132 | Then /^(?:I|they) should see "([^"]*?)" in the email html part body$/ do |text|
133 | current_email.html_part.body.to_s.should include(text)
134 | end
135 |
136 | Then /^(?:I|they) should see "([^"]*?)" in the email text part body$/ do |text|
137 | current_email.text_part.body.to_s.should include(text)
138 | end
139 |
140 | #
141 | # Inspect the Email Attachments
142 | #
143 |
144 | Then /^(?:I|they) should see (an|no|\d+) attachments? with the email$/ do |amount|
145 | current_email_attachments.size.should == parse_email_count(amount)
146 | end
147 |
148 | Then /^there should be (an|no|\d+) attachments? named "([^"]*?)"$/ do |amount, filename|
149 | current_email_attachments.select { |a| a.filename == filename }.size.should == parse_email_count(amount)
150 | end
151 |
152 | Then /^attachment (\d+) should be named "([^"]*?)"$/ do |index, filename|
153 | current_email_attachments[(index.to_i - 1)].filename.should == filename
154 | end
155 |
156 | Then /^there should be (an|no|\d+) attachments? of type "([^"]*?)"$/ do |amount, content_type|
157 | current_email_attachments.select { |a| a.content_type.include?(content_type) }.size.should == parse_email_count(amount)
158 | end
159 |
160 | Then /^attachment (\d+) should be of type "([^"]*?)"$/ do |index, content_type|
161 | current_email_attachments[(index.to_i - 1)].content_type.should include(content_type)
162 | end
163 |
164 | Then /^all attachments should not be blank$/ do
165 | current_email_attachments.each do |attachment|
166 | attachment.read.size.should_not == 0
167 | end
168 | end
169 |
170 | Then /^show me a list of email attachments$/ do
171 | EmailSpec::EmailViewer::save_and_open_email_attachments_list(current_email)
172 | end
173 |
174 | #
175 | # Interact with Email Contents
176 | #
177 |
178 | When /^(?:I|they) follow "([^"]*?)" in the email$/ do |link|
179 | visit_in_email(link)
180 | end
181 |
182 | When /^(?:I|they) click the first link in the email$/ do
183 | click_first_link_in_email
184 | end
185 |
186 | #
187 | # Debugging
188 | # These only work with Rails and OSx ATM since EmailViewer uses RAILS_ROOT and OSx's 'open' command.
189 | # Patches accepted. ;)
190 | #
191 |
192 | Then /^save and open current email$/ do
193 | EmailSpec::EmailViewer::save_and_open_email(current_email)
194 | end
195 |
196 | Then /^save and open all text emails$/ do
197 | EmailSpec::EmailViewer::save_and_open_all_text_emails
198 | end
199 |
200 | Then /^save and open all html emails$/ do
201 | EmailSpec::EmailViewer::save_and_open_all_html_emails
202 | end
203 |
204 | Then /^save and open all raw emails$/ do
205 | EmailSpec::EmailViewer::save_and_open_all_raw_emails
206 | end
207 |
--------------------------------------------------------------------------------
/config/initializers/devise.rb:
--------------------------------------------------------------------------------
1 | # Use this hook to configure devise mailer, warden hooks and so forth.
2 | # Many of these configuration options can be set straight in your model.
3 | Devise.setup do |config|
4 | # ==> Mailer Configuration
5 | # Configure the e-mail address which will be shown in Devise::Mailer,
6 | # note that it will be overwritten if you use your own mailer class with default "from" parameter.
7 | config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
8 |
9 | # Configure the class responsible to send e-mails.
10 | # config.mailer = "Devise::Mailer"
11 |
12 | # ==> ORM configuration
13 | # Load and configure the ORM. Supports :active_record (default) and
14 | # :mongoid (bson_ext recommended) by default. Other ORMs may be
15 | # available as additional gems.
16 | require 'devise/orm/active_record'
17 |
18 | # ==> Configuration for any authentication mechanism
19 | # Configure which keys are used when authenticating a user. The default is
20 | # just :email. You can configure it to use [:username, :subdomain], so for
21 | # authenticating a user, both parameters are required. Remember that those
22 | # parameters are used only when authenticating and not when retrieving from
23 | # session. If you need permissions, you should implement that in a before filter.
24 | # You can also supply a hash where the value is a boolean determining whether
25 | # or not authentication should be aborted when the value is not present.
26 | # config.authentication_keys = [ :email ]
27 |
28 | # Configure parameters from the request object used for authentication. Each entry
29 | # given should be a request method and it will automatically be passed to the
30 | # find_for_authentication method and considered in your model lookup. For instance,
31 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
32 | # The same considerations mentioned for authentication_keys also apply to request_keys.
33 | # config.request_keys = []
34 |
35 | # Configure which authentication keys should be case-insensitive.
36 | # These keys will be downcased upon creating or modifying a user and when used
37 | # to authenticate or find a user. Default is :email.
38 | config.case_insensitive_keys = [ :email ]
39 |
40 | # Configure which authentication keys should have whitespace stripped.
41 | # These keys will have whitespace before and after removed upon creating or
42 | # modifying a user and when used to authenticate or find a user. Default is :email.
43 | config.strip_whitespace_keys = [ :email ]
44 |
45 | # Tell if authentication through request.params is enabled. True by default.
46 | # It can be set to an array that will enable params authentication only for the
47 | # given strategies, for example, `config.params_authenticatable = [:database]` will
48 | # enable it only for database (email + password) authentication.
49 | # config.params_authenticatable = true
50 |
51 | # Tell if authentication through HTTP Basic Auth is enabled. False by default.
52 | # It can be set to an array that will enable http authentication only for the
53 | # given strategies, for example, `config.http_authenticatable = [:token]` will
54 | # enable it only for token authentication.
55 | # config.http_authenticatable = false
56 |
57 | # If http headers should be returned for AJAX requests. True by default.
58 | # config.http_authenticatable_on_xhr = true
59 |
60 | # The realm used in Http Basic Authentication. "Application" by default.
61 | # config.http_authentication_realm = "Application"
62 |
63 | # It will change confirmation, password recovery and other workflows
64 | # to behave the same regardless if the e-mail provided was right or wrong.
65 | # Does not affect registerable.
66 | # config.paranoid = true
67 |
68 | # By default Devise will store the user in session. You can skip storage for
69 | # :http_auth and :token_auth by adding those symbols to the array below.
70 | # Notice that if you are skipping storage for all authentication paths, you
71 | # may want to disable generating routes to Devise's sessions controller by
72 | # passing :skip => :sessions to `devise_for` in your config/routes.rb
73 | config.skip_session_storage = [:http_auth]
74 |
75 | # ==> Configuration for :database_authenticatable
76 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If
77 | # using other encryptors, it sets how many times you want the password re-encrypted.
78 | #
79 | # Limiting the stretches to just one in testing will increase the performance of
80 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
81 | # a value less than 10 in other environments.
82 | config.stretches = Rails.env.test? ? 1 : 10
83 |
84 | # Setup a pepper to generate the encrypted password.
85 | # config.pepper = "eaeb3860d7c0a65080ff0dd2d7b05a9aedc9dab1724fd92eed8777300beb269873f7abec622d91d6c16a8164472cc9921094e122d43f3fae68ff57852862a804"
86 |
87 | # ==> Configuration for :confirmable
88 | # A period that the user is allowed to access the website even without
89 | # confirming his account. For instance, if set to 2.days, the user will be
90 | # able to access the website for two days without confirming his account,
91 | # access will be blocked just in the third day. Default is 0.days, meaning
92 | # the user cannot access the website without confirming his account.
93 | # config.allow_unconfirmed_access_for = 2.days
94 |
95 | # A period that the user is allowed to confirm their account before their
96 | # token becomes invalid. For example, if set to 3.days, the user can confirm
97 | # their account within 3 days after the mail was sent, but on the fourth day
98 | # their account can't be confirmed with the token any more.
99 | # Default is nil, meaning there is no restriction on how long a user can take
100 | # before confirming their account.
101 | # config.confirm_within = 3.days
102 |
103 | # If true, requires any email changes to be confirmed (exactly the same way as
104 | # initial account confirmation) to be applied. Requires additional unconfirmed_email
105 | # db field (see migrations). Until confirmed new email is stored in
106 | # unconfirmed email column, and copied to email column on successful confirmation.
107 | config.reconfirmable = true
108 |
109 | # Defines which key will be used when confirming an account
110 | # config.confirmation_keys = [ :email ]
111 |
112 | # ==> Configuration for :rememberable
113 | # The time the user will be remembered without asking for credentials again.
114 | # config.remember_for = 2.weeks
115 |
116 | # If true, extends the user's remember period when remembered via cookie.
117 | # config.extend_remember_period = false
118 |
119 | # Options to be passed to the created cookie. For instance, you can set
120 | # :secure => true in order to force SSL only cookies.
121 | # config.rememberable_options = {}
122 |
123 | # ==> Configuration for :validatable
124 | # Range for password length. Default is 8..128.
125 | config.password_length = 8..128
126 |
127 | # Email regex used to validate email formats. It simply asserts that
128 | # an one (and only one) @ exists in the given string. This is mainly
129 | # to give user feedback and not to assert the e-mail validity.
130 | # config.email_regexp = /\A[^@]+@[^@]+\z/
131 |
132 | # ==> Configuration for :timeoutable
133 | # The time you want to timeout the user session without activity. After this
134 | # time the user will be asked for credentials again. Default is 30 minutes.
135 | # config.timeout_in = 30.minutes
136 |
137 | # If true, expires auth token on session timeout.
138 | # config.expire_auth_token_on_timeout = false
139 |
140 | # ==> Configuration for :lockable
141 | # Defines which strategy will be used to lock an account.
142 | # :failed_attempts = Locks an account after a number of failed attempts to sign in.
143 | # :none = No lock strategy. You should handle locking by yourself.
144 | # config.lock_strategy = :failed_attempts
145 |
146 | # Defines which key will be used when locking and unlocking an account
147 | # config.unlock_keys = [ :email ]
148 |
149 | # Defines which strategy will be used to unlock an account.
150 | # :email = Sends an unlock link to the user email
151 | # :time = Re-enables login after a certain amount of time (see :unlock_in below)
152 | # :both = Enables both strategies
153 | # :none = No unlock strategy. You should handle unlocking by yourself.
154 | # config.unlock_strategy = :both
155 |
156 | # Number of authentication tries before locking an account if lock_strategy
157 | # is failed attempts.
158 | # config.maximum_attempts = 20
159 |
160 | # Time interval to unlock the account if :time is enabled as unlock_strategy.
161 | # config.unlock_in = 1.hour
162 |
163 | # ==> Configuration for :recoverable
164 | #
165 | # Defines which key will be used when recovering the password for an account
166 | # config.reset_password_keys = [ :email ]
167 |
168 | # Time interval you can reset your password with a reset password key.
169 | # Don't put a too small interval or your users won't have the time to
170 | # change their passwords.
171 | config.reset_password_within = 6.hours
172 |
173 | # ==> Configuration for :encryptable
174 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use
175 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1,
176 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior)
177 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy
178 | # REST_AUTH_SITE_KEY to pepper)
179 | # config.encryptor = :sha512
180 |
181 | # ==> Configuration for :token_authenticatable
182 | # Defines name of the authentication token params key
183 | # config.token_authentication_key = :auth_token
184 |
185 | # ==> Scopes configuration
186 | # Turn scoped views on. Before rendering "sessions/new", it will first check for
187 | # "users/sessions/new". It's turned off by default because it's slower if you
188 | # are using only default views.
189 | # config.scoped_views = false
190 |
191 | # Configure the default scope given to Warden. By default it's the first
192 | # devise role declared in your routes (usually :user).
193 | # config.default_scope = :user
194 |
195 | # Set this configuration to false if you want /users/sign_out to sign out
196 | # only the current scope. By default, Devise signs out all scopes.
197 | # config.sign_out_all_scopes = true
198 |
199 | # ==> Navigation configuration
200 | # Lists the formats that should be treated as navigational. Formats like
201 | # :html, should redirect to the sign in page when the user does not have
202 | # access, but formats like :xml or :json, should return 401.
203 | #
204 | # If you have any extra navigational formats, like :iphone or :mobile, you
205 | # should add them to the navigational formats lists.
206 | #
207 | # The "*/*" below is required to match Internet Explorer requests.
208 | # config.navigational_formats = ["*/*", :html]
209 |
210 | # The default HTTP method used to sign out a resource. Default is :delete.
211 | config.sign_out_via = Rails.env.test? ? :get : :delete
212 |
213 | # ==> OmniAuth
214 | # Add a new OmniAuth provider. Check the wiki for more information on setting
215 | # up on your models and hooks.
216 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
217 |
218 | # ==> Warden configuration
219 | # If you want to use other strategies, that are not supported by Devise, or
220 | # change the failure app, you can configure them inside the config.warden block.
221 | #
222 | # config.warden do |manager|
223 | # manager.intercept_401 = false
224 | # manager.default_strategies(:scope => :user).unshift :some_external_strategy
225 | # end
226 |
227 | # ==> Mountable engine configurations
228 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine
229 | # is mountable, there are some extra configurations to be taken into account.
230 | # The following options are available, assuming the engine is mounted as:
231 | #
232 | # mount MyEngine, at: "/my_engine"
233 | #
234 | # The router that invoked `devise_for`, in the example above, would be:
235 | # config.router_name = :my_engine
236 | #
237 | # When using omniauth, Devise cannot automatically set Omniauth path,
238 | # so you need to do it manually. For the users scope, it would be:
239 | # config.omniauth_path_prefix = "/my_engine/users/auth"
240 | end
241 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. !http://railsapps.github.io/images/rails-36x36.jpg(Rails Application for a Membership, Subscription, or SaaS Site)! Rails App for a Membership, Subscription, or SaaS Site with Recurly
2 |
3 | Rails 3.2 example application with recurring billing using Recurly. Use for a Rails membership site, subscription site, or SaaS site (software-as-a-service). Using:
4 |
5 | * "Devise":http://github.com/plataformatec/devise authentication and user management
6 | * "CanCan":https://github.com/ryanb/cancan authorization for administrator access
7 | * "Twitter Bootstrap":http://twitter.github.com/bootstrap/ front-end framework for CSS styling
8 | * "Recurly":https://recurly.com/ for recurring billing and subscription management
9 |
10 | You can build this application in only a few minutes using the "Rails Composer":http://railsapps.github.io/rails-composer/ tool.
11 |
12 | !http://railsapps.github.io/images/rails-recurly-subscription-saas.png(Rails Application for a Membership, Subscription, or SaaS Site)!
13 |
14 | h2. !http://twitter-badges.s3.amazonaws.com/t_logo-a.png(Follow on Twitter)!:http://www.twitter.com/rails_apps Follow on Twitter
15 |
16 | Follow the project on Twitter: "@rails_apps":http://twitter.com/rails_apps. Please tweet some praise if you like what you've found.
17 |
18 | h2. What Is Implemented -- and What Is Not
19 |
20 | Membership sites restrict access to content such as articles, videos, or user forums. Software-as-a-service (SaaS) sites limit use of web-based software to paid subscribers. The revenue model is the same whether the site provides high-value content or software as a service: A visitor purchases a subscription and gains access to restricted areas of the site. Typically, the subscription is repurchased monthly through a service that provides recurring billing.
21 |
22 | The example application provides a complete and fully functional membership site.
23 |
24 | * tiered pricing for multiple subscription plans
25 | * optional "free trial" subscription as well as free accounts using Recurly
26 | * uses Recurly for no local credit card storage
27 | * Recurly accepts credit card payments from customers in any country or currency
28 | * PCI compliance using the Recurly JavaScript library
29 | * Recurly handles recurring billing, retries if payment fails, and cancels subscription if retries fail
30 | * paid subscriptions are created only after a successful credit card transaction
31 | * subscribers can upgrade or downgrade subscription plans
32 | * subscribers can cancel subscription plans
33 | * configurable subscription renewal period (defaults to one month)
34 | * administrator can change subscription plan or delete user
35 |
36 | h4. What is Not Implemented
37 |
38 | There are additional features you may want for a SaaS application, such as:
39 |
40 | * Basecamp-style subdomains (each user gets their own subdomain)
41 | * "multitenancy":http://en.wikipedia.org/wiki/Multitenancy database segmentation
42 |
43 | These features are not included in this application. See the "rails3-subdomains":https://github.com/RailsApps/rails3-subdomains example application for help with subdomains. For multitenancy, try Brad Robertson's "Apartment":https://github.com/bradrobertson/apartment gem.
44 |
45 | h2. Similar Examples and Tutorials
46 |
47 | h4. RailsApps
48 |
49 | This is one in a series of Rails example apps and tutorials from the "RailsApps Project":http://railsapps.github.io/. See a list of additional "Rails examples, tutorials, and starter apps":http://railsapps.github.io/rails-examples-tutorials.html.
50 |
51 | This example application is based on the "rails3-bootstrap-devise-cancan":https://github.com/RailsApps/rails3-bootstrap-devise-cancan starter application. This example application uses ActiveRecord and a SQLite database with RSpec and Cucumber for testing.
52 |
53 | This application is similar to the "rails-stripe-membership-saas":https://github.com/RailsApps/rails-stripe-membership-saas application which provides recurring billing using the "Stripe":https://stripe.com/ billing service.
54 |
55 | You might also be interested in the "rails-prelaunch-signup":https://github.com/RailsApps/rails-prelaunch-signup example and tutorial from the RailsApps project.
56 |
57 | h4. Other Projects
58 |
59 | * Recurly's "recurly-client-ruby-demo":https://github.com/recurly/recurly-client-ruby-demo
60 | * Recurly's "recurly-client-ruby":https://github.com/recurly/recurly-client-ruby
61 | * Nick O'Neill's "recurly-rails":https://github.com/biznickman/recurly-rails
62 |
63 | h2. Dependencies
64 |
65 | Before generating your application, you will need:
66 |
67 | * The Ruby language (version 1.9.3 or 2.0.0)
68 | * The Rails gem (version 3.2.13)
69 |
70 | See the article "Installing Rails":http://railsapps.github.io/installing-rails.html for advice about updating Rails and your development environment.
71 |
72 | h2. Getting the Application
73 |
74 | You have several options for getting the code. You can _fork_, _clone_, or _generate_.
75 |
76 | h3. Fork
77 |
78 | If you'd like to add features (or bug fixes) to improve the example application, you can fork the GitHub repo and "make pull requests":http://help.github.com/send-pull-requests/. Your code contributions are welcome!
79 |
80 | h3. Clone
81 |
82 | If you want to copy and customize the app with changes that are only useful for your own project, you can clone the GitHub repo. You'll need to search-and-replace the project name throughout the application. You probably should generate the app instead (see below). To clone:
83 |
84 |
87 |
88 | You'll need "git":http://git-scm.com/ on your machine. See "Rails and Git":http://railsapps.github.io/rails-git.html.
89 |
90 | h3. Generate
91 |
92 | If you want to use the project as a starter app, use the "Rails Composer":http://railsapps.github.io/rails-composer/ tool to generate a new version of the example app. You'll be able to give it your own project name when you generate the app. Generating the application gives you additional options.
93 |
94 | To build the example application, run the command:
95 |
96 |
99 |
100 | Use the @-T@ flag to skip Test::Unit files.
101 |
102 | The @$@ character indicates a shell prompt; don't include it when you run the command.
103 |
104 | This creates a new Rails app named @rails-recurly-subscription-saas@ on your computer. You can use a different name if you wish.
105 |
106 | You'll see a prompt:
107 |
108 |
109 | question Install an example application?
110 | 1) I want to build my own application
111 | 2) membership/subscription/saas
112 | 3) rails-prelaunch-signup
113 | 4) rails3-bootstrap-devise-cancan
114 | 5) rails3-devise-rspec-cucumber
115 | 6) rails3-mongoid-devise
116 | 7) rails3-mongoid-omniauth
117 | 8) rails3-subdomains
118 |
125 | question Billing with Stripe or Recurly?
126 | 1) Stripe
127 | 2) Recurly
128 |
129 |
130 | The application generator template will ask you for additional preferences:
131 |
132 |
133 | question Web server for development?
134 | 1) WEBrick (default)
135 | 2) Thin
136 | 3) Unicorn
137 | 4) Puma
138 | question Web server for production?
139 | 1) Same as development
140 | 2) Thin
141 | 3) Unicorn
142 | 4) Puma
143 | question Template engine?
144 | 1) ERB
145 | 2) Haml
146 | 3) Slim
147 | extras Set a robots.txt file to ban spiders? (y/n)
148 | extras Use or create a project-specific rvm gemset? (y/n)
149 | extras Create a GitHub repository? (y/n)
150 |
151 |
152 | h4. Web Servers
153 |
154 | We recommend Thin in development for speed and less noise in the log files.
155 |
156 | If you plan to deploy to Heroku, select Thin as your production webserver.
157 |
158 | h4. Template Engine
159 |
160 | The example application uses the default "ERB" Rails template engine. Optionally, you can use another template engine, such as Haml or Slim. See instructions for "Haml and Rails":http://railsapps.github.io/rails-haml.html.
161 |
162 | h4. Other Choices
163 |
164 | Set a robots.txt file to ban spiders if you want to keep your new site out of Google search results.
165 |
166 | It is a good idea to use "rvm":https://rvm.io/, the Ruby Version Manager, and create a project-specific rvm gemset (not available on Windows). See "Installing Rails":http://railsapps.github.io/installing-rails.html.
167 |
168 | If you choose to create a GitHub repository, the generator will prompt you for a GitHub username and password.
169 |
170 | h4. Troubleshooting
171 |
172 | If you get an error "OpenSSL certificate verify failed" or "Gem::RemoteFetcher::FetchError: SSL_connect" see the article "OpenSSL errors and Rails":http://railsapps.github.io/openssl-certificate-verify-failed.html.
173 |
174 | If you get an error like this:
175 |
176 |
177 | Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
178 | composer Running 'after bundler' callbacks.
179 | The template [...] could not be loaded.
180 | Error: You have already activated ..., but your Gemfile requires ....
181 | Using bundle exec may solve this.
182 |
183 |
184 | It's due to conflicting gem versions. See the article "Rails Error: “You have already activated (…)”":http://railsapps.github.io/rails-error-you-have-already-activated.html.
185 |
186 | h3. Edit the README
187 |
188 | If you're storing the app in a GitHub repository, please edit the README files to add a description of the app and your contact info. If you don't change the README, people will think I am the author of your version of the application.
189 |
190 | h2. Getting Started
191 |
192 | See the article "Installing Rails":http://railsapps.github.io/installing-rails.html to make sure your development environment is prepared properly.
193 |
194 | h3. Recurly Account
195 |
196 | The application implements recurring billing using "Recurly":https://recurly.com/. Before you start, go to the Recurly website and set up an account.
197 |
198 | h3. Merchant Account
199 |
200 | Your business will need a merchant account in order to accept credit card payments. Recurly does not include a merchant account as part of the service. If your business is established and already taking credit card payments, you will already have a merchant account. If not, you will have to obtain a merchant account. If you have a U.S.-based business, Recurly will refer you to their partner, "TSYS Merchant Solutions":http://www.tsysmerchantsolutions.com/mark/index.html, one of the largest credit card processors in the U.S., and a salesperson will contact you to send you an application form. It takes a few days to get approval so get the process started early.
201 |
202 | h3. Use RVM
203 |
204 | I recommend using "rvm":https://rvm.io/, the Ruby Version Manager, to create a project-specific gemset for the application. If you generate the application with the Rails Composer tool, you can create a project-specific gemset.
205 |
206 | h3. Install the Required Gems
207 |
208 | Check the Gemfile to see which gems are used by this application.
209 |
210 | If you generate the application with the Rails Composer tool, all gems will be installed.
211 |
212 | Otehrwise, run the @bundle install@ command to install the required gems on your computer:
213 |
214 |
215 | $ bundle install
216 |
217 |
218 | You can check which gems are installed on your computer with:
219 |
220 |
221 | $ gem list
222 |
223 |
224 | Keep in mind that you have installed these gems locally. When you deploy the app to another server, the same gems (and versions) must be available.
225 |
226 | h3. Configure Email
227 |
228 | You must configure the application for your email account. See the article "Send Email with Rails":http://railsapps.github.io/rails-send-email.html.
229 |
230 | h3. Configure Devise
231 |
232 | You can modify the configuration file for Devise if you want to use something other than the defaults:
233 |
234 | * *config/initializers/devise.rb*
235 |
236 | h3. Configuration File
237 |
238 | The application uses the "figaro gem":https://github.com/laserlemon/figaro to set environment variables. Credentials for your administrator account and Recurly API and cryptographic keys are set in the *config/application.yml* file. The *.gitignore* file prevents the *config/application.yml* file from being saved in the git repository so your credentials are kept private. See the article "Rails Environment Variables":http://railsapps.github.io/rails-environment-variables.html for more information.
239 |
240 | Modify the file *config/application.yml*:
241 |
242 |
243 | # Add account credentials and API keys here.
244 | # See http://railsapps.github.io/rails-environment-variables.html
245 | # This file should be listed in .gitignore to keep your settings secret!
246 | # Each entry sets a local environment variable and overrides ENV variables in the Unix shell.
247 | # For example, setting:
248 | # GMAIL_USERNAME: Your_Gmail_Username
249 | # makes 'Your_Gmail_Username' available as ENV["GMAIL_USERNAME"]
250 | # Add application configuration variables here, as shown below.
251 | #
252 | GMAIL_USERNAME: Your_Username
253 | GMAIL_PASSWORD: Your_Password
254 | ADMIN_FIRST_NAME: First
255 | ADMIN_LAST_NAME: User
256 | ADMIN_EMAIL: user@example.com
257 | ADMIN_PASSWORD: changeme
258 | ROLES: [admin, silver, gold, platinum]
259 | RECURLY_API_KEY: = recurly_api_key
260 | RECURLY_JS_PRIVATE_KEY: = recurly_js_private_key
261 | RECURLY_SUBDOMAIN: myapp
262 |
263 |
264 | Set the user name and password needed for the application to send email.
265 |
266 | If you wish, set your name, email address, and password for an administrator's account. If you prefer, you can use the default to sign in to the application and edit the account after deployment. It is always a good idea to change the administrator's password after the application is deployed.
267 |
268 | The roles you specify in the configuration file are the subscription plans that will be available to the application's users. You will need an "admin" role. Keep the "silver", "gold", and "platinum" roles while you are testing the application. You can change these roles later, after you familiarize yourself with the application and begin to customize it for your own needs.
269 |
270 | The Recurly gem requires an API key to operate. We'll also need to supply a private key so the Recurly gem can generate a cryptographic signature. You can find both keys on your Recurly account profile under "API Credentials".
271 |
272 | Set a @RECURLY_SUBDOMAIN@ environment variable that is used in the JavaScript code that initiates the Recurly transaction. Recurly creates a customer-facing website and API interface with a subdomain on the Recurly website. The URL will look like this: @https://myapp.recurly.com/@ where "myapp" is the name of your company or service. You'll use the subdomain "myapp" to form the URL that makes requests to the Recurly API interface. The Recurly subdomain is listed on your Recurly account profile.
273 |
274 | All configuration values in the *config/application.yml* file are available anywhere in the application as environment variables. For example, @ENV["GMAIL_USERNAME"]@ will return the string "Your_Username".
275 |
276 | If you prefer, you can delete the *config/application.yml* file and set each value as an environment variable in the Unix shell.
277 |
278 | h3. Set Up a Database Seed File
279 |
280 | The *db/seeds.rb* file initializes the database with default values. To keep some data private, and consolidate configuration settings in a single location, we use the *config/application.yml* file to set environment variables and then use the environment variables in the *db/seeds.rb* file.
281 |
282 |
300 |
301 | The *db/seeds.rb* file reads a list of roles from the *config/application.yml* file and adds the roles to the database. In fact, any new role can be added to the roles datatable with a statement such @user.add_role :superhero@. Setting the roles in the *db/seeds.rb* file simply makes sure each role is listed and available should a user wish to change roles.
302 |
303 | We add an administrator and three sample users. Our User model has separate first and last names to accommodate Recurly’s customer schema.
304 |
305 | Values from the *config/application.yml* file are used to create a user with an administrator role. You can log in with this account for access as an administrator.
306 |
307 | You can change the administrator name, email, and password in this file but it is better to make the changes in the *config/application.yml* file to keep the credentials private. If you decide to include your private password in the *db/seeds.rb* file, be sure to add the filename to your *.gitignore* file so that your password doesn't become available in your public GitHub repository.
308 |
309 | Note that it's not necessary to personalize the *db/seeds.rb* file before you deploy your app. You can deploy the app with an example user and then use the application's "Edit Account" feature to change name, email address, and password after you log in. Use this feature to log in as an administrator and change the user name and password to your own.
310 |
311 | h3. Set the Database
312 |
313 | Prepare the database and add the default user to the database by running the commands:
314 |
315 |
319 |
320 | Use @rake db:reset@ if you want to empty and reseed the database.
321 |
322 | Set the database for running tests:
323 |
324 |
325 | $ rake db:test:prepare
326 |
327 |
328 | If you’re not using "rvm":https://rvm.io/, the Ruby Version Manager, you should preface each rake command with @bundle exec@. You don’t need to use @bundle exec@ if you are using rvm version 1.11.0 or newer.
329 |
330 | h3. The Recurly Initializer
331 |
332 | The file *config/initializers/recurly.rb* sets the Recurly API key and cryptographic private key from environment variables:
333 |
334 |
340 |
341 | You could hardcode the Recurly API key and cryptographic private key in the *config/initializers/recurly.rb* file but instead, we advise to set the Recurly API key and cryptographic private key in the *config/application.yml* file. Recording sensitive information in the *config/initializers/recurly.rb* file might expose it publicly on a GitHub repo. Both the Recurly API key and cryptographic private key should be kept secret; the Recurly subdomain can be revealed without consequence.
342 |
343 | If you're not operating with US dollars as your currency, you can change the default in the initializer file.
344 |
345 | h3. Prepare Your Recurly Account
346 |
347 | Before we can submit a billing request to Recurly, we have to set up our Recurly account.
348 |
349 | First, take a look at "Site Settings" in your Recurly account profile. Recurly offers options for "Address Requirement." By default, Recurly expects a full address, including street, city, state and postal code. Our example application only asks for the user's country (for enhanced fraud protection). Change the "Address Requirement" setting to "No Address." If you don't change the "Address Requirement" setting from the default, you'll see an error when you test the application: "Billing info address1 can't be empty, Billing info zip can't be empty, Billing info city can't be empty, Billing info state can't be empty."
350 |
351 | Next we'll set up our subscription plans. We'll tell Recurly that we have three plans named "Silver", "Gold", and "Platinum" that will be billed monthly at rates of $9, $19, and $29. Once a customer is created and assigned a plan, Recurly will do all the work of notifying the user, initiating monthly billing, and contacting the user when a credit card is declined or expires.
352 |
353 | Go to your Recurly account profile to create a subscription plan. Look for "Configuration/Subscription Plans." Recurly offers "documentation about creating subscriptions":https://docs.recurly.com/subscriptions and "additional detail about subscriptions":https://docs.recurly.com/api/subscriptions.
354 |
355 | Create three different plans with the following values:
356 |
357 | |_. Plan Name |_. Plan Code |_. Pricing |_. Interval |
358 | | Silver | silver | 9.00 | monthly |
359 | | Gold | gold | 19.00 | monthly |
360 | | Platinum | platinum | 29.00 | monthly |
361 |
362 | "Plan Name" is displayed on invoices and in the Recurly web interface. "Plan Code" is a unique string of your choice that is used to identify the plan when subscribing a customer. The "Plan Code" should correspond to a role we've created to manage access. "Pricing" is the subscription price. You'll specify the billing frequency. Optionally, you can specify a free trial period. If you include a trial period, the customer won't be billed for the first time until the trial period ends. If the customer cancels before the trial period is over, she'll never be billed at all.
363 |
364 | h3. Recurly Push Notifications
365 |
366 | When a credit card expires or a monthly transaction is declined, Recurly will automatically retry a recurring payment after it fails. After a number of attempts (set in your Recurly "Dunning Management" settings), Recurly will cancel the subscription. Your application needs to know to deny access for a subscriber with an expired account. Recurly provides "webhooks":http://en.wikipedia.org/wiki/Webhook (push notifications) to communicate events to you (for details, see the "Recurly Push Notifications documentation":http://docs.recurly.com/push-notifications and "Recurly Push Notifications API":http://docs.recurly.com/api/push-notifications).
367 |
368 | A Recurly push notification is an HTTP request from Recurly's servers to your site. It is not a visit to your website from a web browser; rather it is an HTTP POST request (like a form submission) to your application from the Recurly servers. The HTTP request contains XML data that provides data about the event, including a customer account code that can be used to retrive the data from the Recurly server. It is best to ignore the event data (because it could be falsified) and query the Recurly server to obtain the subscription status.
369 |
370 | The example application only responds to "expired_subscription_notification" events. You can customize the application to respond to other events.
371 |
372 | For push notifications to work, you must visit your Recurly dashboard and enter the URL in your "Push Notifications" settings:
373 |
374 | * http://www.example.com/recurly/push
375 |
376 | h3. Change your Application's Secret Token
377 |
378 | If you've used the Rails Composer tool to generate the application, the application's secret token will be unique, just as with any Rails application generated with the @rails new@ command.
379 |
380 | However, if you've cloned the application directly from GitHub, it is crucial that you change the application's secret token before deploying your application in production mode. Otherwise, people could change their session information, and potentially access your SaaS or membership site as a premium user or administrator. Your secret token should be at least 30 characters long and completely random.
381 |
382 | Get a unique secret token:
383 |
384 |
385 | rake secret
386 |
387 |
388 | Edit your *config/initializers/secret_token.rb* file to add the secret token:
389 |
390 |
393 |
394 | h2. Test the App
395 |
396 | You can check that your app runs properly by entering the command:
397 |
398 | @$ rails server@
399 |
400 | To see your application in action, open a browser window and navigate to "http://localhost:3000/":http://localhost:3000.
401 |
402 | If you are using the default values from the *config/application.yml* file, you can sign in as the administrator using:
403 |
404 | * email: user@example.com
405 | * password: changeme
406 |
407 | You'll see a navigation link for Admin. Clicking the link will display a page with a list of users at
408 | "http://localhost:3000/users":http://localhost:3000/users.
409 |
410 | To sign in as the second user, use
411 |
412 | * email: user2@example.com
413 | * password: changeme
414 |
415 | The second user will not see the Admin navigation link and will not be able to access the page at
416 | "http://localhost:3000/users":http://localhost:3000/users.
417 |
418 | You should be able to create additional users using the fake credit card number _4111111111111111_. You'll see the new users listed when you log in as an administrator. And you'll see the new users listed as customers when you visit your Recurly dashboard.
419 |
420 | Stop the server with Control-C.
421 |
422 | If you test the app by starting the web server and then leave the server running while you install new gems, you’ll have to restart the server to see any changes. The same is true for changes to configuration files in the config folder. This can be confusing to new Rails developers because you can change files in the app folders without restarting the server. Stop the server each time after testing and you will avoid this issue.
423 |
424 | h2. Deploy to Heroku
425 |
426 | For your convenience, here is a "Tutorial for Rails on Heroku":http://railsapps.github.io/rails-heroku-tutorial.html. Heroku provides low cost, easily configured Rails application hosting.
427 |
428 | Be sure to set up SSL before you make your application available in production. See the "Heroku documentation on SSL":https://devcenter.heroku.com/articles/ssl.
429 |
430 | Prior to deployment, change your *db/seeds.rb* file. Remove the "example.com" sample users.
431 |
432 |
443 |
444 | You'll need to set the configuration values from the *config/application.yml* file as Heroku environment variables. See the article "Rails Environment Variables":http://railsapps.github.io/rails-environment-variables.html for more information.
445 |
446 | With the figaro gem, just run:
447 |
448 |
449 | rake figaro:heroku
450 |
451 |
452 | Alternatively, you can set Heroku environment variables directly with @heroku config:add@.
453 |
454 |