Skip to content

Commit

Permalink
WIP stripe integration
Browse files Browse the repository at this point in the history
  • Loading branch information
danallison committed Jul 27, 2021
1 parent 6631059 commit ea82aa5
Show file tree
Hide file tree
Showing 13 changed files with 446 additions and 46 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Change these variables to match your environment
SECRET_KEY_BASE=**exec `rails secret` and put the output here**
SHARED_SECRET=**only needed if using custom external app integration**
APP_BASE_URL=http://localhost:3000
CALCULIST_MAIN_URL=https://calculist.io
CALCULIST_DATABASE=**your database**
CALCULIST_DATABASE_USER=**your db user**
CALCULIST_DATABASE_PASSWORD=**strong password**
Expand All @@ -9,6 +11,11 @@ LETSENCRYPT_HOST=my.domain.com
LETSENCRYPT_EMAIL=[email protected]
MAILGUN_USERNAME=**your mg smtp username**
MAILGUN_PASSWORD=**your mg smtp password**
STRIPE_PUBLISHABLE_KEY=**not needed unless charging for service**
STRIPE_SECRET_KEY=**not needed unless charging for service**
STRIPE_PERSONAL_PLAN_ID=**not needed unless charging for service**
STRIPE_PRO_PLAN_ID=**not needed unless charging for service**

# The variables below do not need to be changed
RAILS_ENV=production
RAILS_SERVE_STATIC_FILES=true
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source 'https://rubygems.org'


gem 'stripe'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.7', '>= 5.0.7.2'
# Use mysql as the database for Active Record
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
stripe (5.32.1)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
Expand Down Expand Up @@ -201,6 +202,7 @@ DEPENDENCIES
sass-rails (~> 5.0)
spring
spring-watcher-listen (~> 2.0.0)
stripe
tzinfo-data
uglifier (>= 1.3.0)
web-console
Expand Down
99 changes: 99 additions & 0 deletions app/controllers/subscriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
class SubscriptionsController < ApplicationController
# 1. Select a plan and enter your email.
# 2. Checkout through stripe.
# 3. If you do not have an account, create your username and password.
def index
if current_user
# Does this user already have a stripe customer record?
if current_stripe_customer
# Does this user already have a subscription?
# TODO Tell them how to contact us to make updates to their subscription
render json: { message: 'already customer' }
return
elsif params[:plan]
# TODO attach user id to stripe customer metadata
stripe_checkout_session = sh.create_checkout_session(
params[:plan], current_user.email, current_user.id
)
@plan = params[:plan]
@stripe_checkout_session_id = stripe_checkout_session['id']
@current_user_email = current_user.email
render 'devise/registrations/stripe_checkout'
return
else
redirect_to "#{ENV['CALCULIST_MAIN_URL']}/pricing"
end
elsif params[:plan]
redirect_to "/join?plan=#{params[:plan]}"
else
redirect_to "#{ENV['CALCULIST_MAIN_URL']}/pricing"
end
end

def show
if current_user
end
end

def complete_checkout
if params[:session_id]
stripe_checkout_session = sh.get_checkout_session(params[:session_id])
if stripe_checkout_session && stripe_checkout_session['payment_status'] == 'paid'
invite_code = BetaAccess.create(
notes: "stripe:#{stripe_checkout_session['customer']}",
claimed_by: current_user ? current_user.id : nil,
claimed_at: current_user ? DateTime.now : nil,
).code
if current_user
# redirect to ...
render 'thankyou'
else
email = stripe_checkout_session['customer_details']['email']
redirect_to "/join?email=#{email}&code=#{invite_code}"
end
else
# redirect ???
raise 'error'
end
end
end

def get_stripe_checkout_session
if params[:email] && params[:plan] && sh.valid_plan?(params[:plan])
if current_user && current_user.email != params[:email]
# error
elsif current_user && current_user.email == params[:email]
stripe_checkout_session = sh.create_checkout_session(params[:plan], params[:email])
elsif current_user.nil?
existing_user = User.where(email: params[:email]).first
if existing_user
render status: 409, json: { message: 'email taken' }
return
else
stripe_checkout_session = sh.create_checkout_session(params[:plan], params[:email])
end
end
end

if stripe_checkout_session
render json: {
stripe_checkout_session_id: stripe_checkout_session['id'],
plan: params[:plan],
}
else
# error message
end
end

private

def current_stripe_customer
return nil unless current_user
return @csc if defined?(@csc)
@csc = sh.get_customer_for_user(current_user.id)
end

def sh
@sh ||= StripeHelper.new
end
end
16 changes: 12 additions & 4 deletions app/controllers/users/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]

# GET /resource/sign_up
# def new
# super
# end
# GET /join
def new
sh = StripeHelper.new
if params[:plan] && sh.valid_plan?(params[:plan])
@plan = params[:plan]
render 'stripe_checkout'
elsif params[:invite_code] || params[:code]
super
else
redirect_to "#{ENV['CALCULIST_MAIN_URL']}/pricing"
end
end

# POST /resource
# def create
Expand Down
54 changes: 54 additions & 0 deletions app/services/stripe_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class StripeHelper
def initialize(secret_key: Rails.application.secrets.stripe_secret_key)
Stripe.api_key = secret_key
end

def valid_plan?(plan)
plan == 'personal' || plan == 'professional'
end

def get_products
Stripe::Product.list
end

def get_customers
Stripe::Customer.list
end

def get_customer_for_user(user_id)
user = User.find(user_id)
return nil unless user
ba = BetaAccess
.where(claimed_by: user_id)
.where("notes like 'stripe:cus%'").first
return nil unless ba
sc_id = ba.notes.split(':')[1]
sc = Stripe::Customer.retrieve(sc_id)
sc
end

def price_from_plan(plan)
{
'personal' => ENV['STRIPE_PERSONAL_PLAN_ID'],
'professional' => ENV['STRIPE_PRO_PLAN_ID']
}[plan]
end

def create_checkout_session(plan, customer_email = nil, client_reference_id = nil)

Stripe::Checkout::Session.create(
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: price_from_plan(plan), quantity: 1 }],
# customer: customer,
client_reference_id: client_reference_id,
customer_email: customer_email,
success_url: "#{ENV['APP_BASE_URL']}/subscribe/complete_checkout?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "#{ENV['CALCULIST_MAIN_URL']}/pricing?cancel=true",
)
end

def get_checkout_session(session_id)
Stripe::Checkout::Session.retrieve(session_id)
end
end
145 changes: 105 additions & 40 deletions app/views/devise/registrations/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,50 +1,104 @@
<div class="devise-form">
<a href="https://calculist.io"><img class="logo-icon" src="/logo-icon-blue-black.svg"></a>
<h2>Join Calculist</h2>

<div style="text-align:center;margin: 20px 0;">
<div id="message">Need an <a style="text-decoration:underline;" href="https://eepurl.com/cxt3mb">invite code</a>?</div>
</div>
<div class="form-inputs">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<style media="screen">
body { background: #fff; }
#main-container.theme-light { background: #fff; }
form em {
font-style: normal;
font-size: 0.8rem;
color: #a3a3a3;
}
#error_explanation h2 {
font-size: 1rem;
}
.gg-check-o {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs,1));
width: 22px;
height: 22px;
border: 2px solid;
border-radius: 100px
}

<div class="field">
<%= f.label :invite_code %><br />
<%= f.text_field :invite_code, autofocus: true %>
.gg-check-o::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
left: 3px;
top: -1px;
width: 6px;
height: 10px;
border-color: currentColor;
border-width: 0 2px 2px 0;
border-style: solid;
transform-origin: bottom left;
transform: rotate(45deg)
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-alpha.4/css/materialize.min.css" integrity="sha384-VP0GfisErC22dnswxVzLKdy1z+wIV3p/iGyahqdhsuFvfu9wrRUaXUAdLWPN7E8m" crossorigin="anonymous">
<!-- TODO Combine this page with the existing join page -->
<div class="container">
<div class="section">
<div class="row center" style="text-align:justify;margin:0 auto;">
<div class="col s8 offset-s2">
<div id="step-1-container" style="opacity: 0.3; ">
<h4 id="step-1-header">Step 1. Enter your email <i id="step-1-check" class="gg-check-o" style="display:inline-block;background:green;color:white;"></i></h4>
</div>
</div>
</div>

<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username %>
<div class="row center" style="text-align:justify;margin:0 auto;">
<div class="col s8 offset-s2">
<div id="step-2-container" style="opacity: 0.3; margin-top: 10px;">
<h4>Step 2. Add payment info <i id="step-2-check" class="gg-check-o" style="display:inline-block;background:green;color:white;"></i></h4>
</div>
</div>
</div>

<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email %>
</div>
<div class="row center" style="text-align:justify;margin:0 auto;">
<div class="col s8 offset-s2">
<div id="step-3-container" style="margin-top: 10px;">
<h4>Step 3. Create a username and password</h4>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>

<div class="field">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %>+ characters)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field" style="display:none;">
<%= f.label :invite_code %><br />
<%= f.text_field :invite_code %>
</div>

<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email %>
</div>

<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
</div>
<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username, autofocus: true %>
</div>

<div class="field">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %>+ characters)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>

<%= render "devise/shared/links" %>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>

<div class="actions">
<%= f.submit "Sign up", class: "btn-large waves-effect waves-light light-blue" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>

<script>
Expand All @@ -57,9 +111,20 @@
}
return params;
}, {});
if (params.invite_code) {
document.getElementById('user_invite_code').value = params.invite_code;
document.getElementById('message').innerHTML = '';
if (params.invite_code || params.code) {
var inviteCodeInput = document.getElementById('user_invite_code');
inviteCodeInput.value = params.invite_code || params.code;
inviteCodeInput.setAttribute('readonly', true);
}
if (params.email) {
var emailInput = document.getElementById('user_email')
emailInput.value = params.email;
emailInput.setAttribute('readonly', true);

var pageTitleEl = document.getElementById('page-title');
pageTitleEl.textContent = 'Almost done!';


}
}
</script>
Loading

0 comments on commit ea82aa5

Please sign in to comment.