koudoku 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +41 -9
- data/app/assets/stylesheets/koudoku/pricing-table.scss +10 -9
- data/app/concerns/koudoku/subscription.rb +22 -1
- data/app/controllers/koudoku/application_controller.rb +1 -1
- data/app/controllers/koudoku/subscriptions_controller.rb +66 -8
- data/app/views/koudoku/subscriptions/_card.html.erb +97 -0
- data/app/views/koudoku/subscriptions/_pricing_table.html.erb +8 -21
- data/app/views/koudoku/subscriptions/_social_proof.html.erb +11 -0
- data/app/views/koudoku/subscriptions/_stripe_js.html.erb +0 -0
- data/app/views/koudoku/subscriptions/edit.html.erb +7 -3
- data/app/views/koudoku/subscriptions/new.html.erb +1 -99
- data/app/views/koudoku/subscriptions/show.html.erb +12 -5
- data/config/routes.rb +1 -0
- data/lib/generators/koudoku/install_generator.rb +8 -3
- data/lib/koudoku/engine.rb +1 -0
- data/lib/koudoku/version.rb +1 -1
- metadata +25 -6
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Koudoku
|
2
2
|
|
3
|
-
Robust subscription support for Ruby on Rails apps using [Stripe](https://stripe.com).
|
3
|
+
Robust subscription support for Ruby on Rails apps using [Stripe](https://stripe.com), including out-of-the-box pricing pages, payment pages, and subscription management for your customers. Also makes it easy to manage logic related to new subscriptions, upgrades, downgrades, cancellations, payment failures, and streamlines hooking up notifications, metrics logging, etc.
|
4
|
+
|
5
|
+
To see an example of Koudoku in action, please visit [Koudoku.org](http://koudoku.org/).
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
@@ -12,11 +14,34 @@ After running `bundle install`, you can run a Rails generator to do the rest. Be
|
|
12
14
|
|
13
15
|
rails g koudoku:install user
|
14
16
|
|
15
|
-
After installing, you'll need to add some subscription plans.
|
16
|
-
|
17
|
-
Plan.create(
|
18
|
-
|
19
|
-
|
17
|
+
After installing, you'll need to add some subscription plans. (Note that we highlight the 'Team' plan.)
|
18
|
+
|
19
|
+
Plan.create({
|
20
|
+
name: 'Personal',
|
21
|
+
price: 10.00,
|
22
|
+
stripe_id: '1', # This maps to the plan ID in Stripe.
|
23
|
+
features: ['1 Project', '1 Page', '1 User', '1 Organization'].join("\n\n"), # Features support Markdown syntax.
|
24
|
+
display_order: 1
|
25
|
+
})
|
26
|
+
|
27
|
+
Plan.create({
|
28
|
+
name: 'Team',
|
29
|
+
highlight: true, # This highlights the plan on the pricing page.
|
30
|
+
price: 30.00,
|
31
|
+
stripe_id: '2',
|
32
|
+
features: ['3 Projects', '3 Pages', '3 Users', '3 Organizations'].join("\n\n"),
|
33
|
+
display_order: 2
|
34
|
+
})
|
35
|
+
|
36
|
+
Plan.create({
|
37
|
+
name: 'Enterprise',
|
38
|
+
price: 100.00,
|
39
|
+
stripe_id: '3',
|
40
|
+
features: ['10 Projects', '10 Pages', '10 Users', '10 Organizations'].join("\n\n"),
|
41
|
+
display_order: 3
|
42
|
+
})
|
43
|
+
|
44
|
+
The only view installed locally into your app by default is the `koudoku/subscriptions/_social_proof.html.erb` partial which is displayed alongside the pricing table. It's designed as a placeholder where you can provide quotes about your product from customers that could positively influence your visitors.
|
20
45
|
|
21
46
|
### Configuring Stripe API Keys
|
22
47
|
|
@@ -36,9 +61,13 @@ On Heroku you accomplish this same effect with [Config Vars](https://devcenter.h
|
|
36
61
|
|
37
62
|
## User-Facing Subscription Management
|
38
63
|
|
39
|
-
|
64
|
+
By default a `pricing_path` route is defined which you can link to in order to show visitors a pricing table. If a user is signed in, this pricing table will take into account their current plan. For example, you can link to this page like so:
|
40
65
|
|
41
|
-
|
66
|
+
<%= link_to 'Pricing', pricing_path %>
|
67
|
+
|
68
|
+
Existing users can view available plans, select a plan, enter credit card details, review their subscription, change plans, and cancel at the following route:
|
69
|
+
|
70
|
+
koudoku.owner_subscriptions_path(@user)
|
42
71
|
|
43
72
|
In these paths, `owner` refers to `User` by default, or whatever model has been configured to be the owner of the `Subscription` model.
|
44
73
|
|
@@ -56,7 +85,6 @@ Or, if you've replaced your `application.css` with an `application.scss` (like I
|
|
56
85
|
|
57
86
|
@import "koudoku/pricing-table"
|
58
87
|
|
59
|
-
|
60
88
|
## Using Coupons
|
61
89
|
|
62
90
|
While more robust coupon support is expected in the future, the simple way to use a coupon is to first create it:
|
@@ -69,6 +97,8 @@ Then assign it to a _new_ subscription before saving:
|
|
69
97
|
subscription.coupon = coupon
|
70
98
|
subscription.save
|
71
99
|
|
100
|
+
It should be noted that these coupons are different from the coupons provided natively by Stripe.
|
101
|
+
|
72
102
|
## Implementing Logging, Notifications, etc.
|
73
103
|
|
74
104
|
The included module defines the following empty "template methods" which you're able to provide an implementation for in `Subscription`:
|
@@ -78,11 +108,13 @@ The included module defines the following empty "template methods" which you're
|
|
78
108
|
- `prepare_for_upgrade`
|
79
109
|
- `prepare_for_downgrade`
|
80
110
|
- `prepare_for_cancelation`
|
111
|
+
- `prepare_for_card_update`
|
81
112
|
- `finalize_plan_change!`
|
82
113
|
- `finalize_new_subscription!`
|
83
114
|
- `finalize_upgrade!`
|
84
115
|
- `finalize_downgrade!`
|
85
116
|
- `finalize_cancelation!`
|
117
|
+
- `finalize_card_update!`
|
86
118
|
- `card_was_declined`
|
87
119
|
|
88
120
|
Be sure to include a call to `super` in each of your implementations, especially if you're using multiple concerns to break all this logic into smaller pieces.
|
@@ -1,5 +1,5 @@
|
|
1
1
|
ul.koudoku-pricing-table {
|
2
|
-
|
2
|
+
|
3
3
|
position: relative;
|
4
4
|
margin: 20px 0;
|
5
5
|
margin-left: 25px;
|
@@ -17,7 +17,6 @@ ul.koudoku-pricing-table {
|
|
17
17
|
position: relative;
|
18
18
|
z-index: 99;
|
19
19
|
|
20
|
-
|
21
20
|
&.plan-primary {
|
22
21
|
background-color: #f9f9f9;
|
23
22
|
margin-top: 0px;
|
@@ -26,15 +25,17 @@ ul.koudoku-pricing-table {
|
|
26
25
|
margin-right: -5px;
|
27
26
|
|
28
27
|
z-index: 100;
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
28
|
+
|
29
|
+
h3 {
|
30
|
+
font-size: 32.5px;
|
31
|
+
line-height: 51px;
|
32
|
+
}
|
33
|
+
h4 {
|
34
|
+
line-height: 22px;
|
34
35
|
}
|
35
36
|
a, input[type=submit] {
|
36
|
-
font-size:
|
37
|
-
|
37
|
+
font-size: 21.5px;
|
38
|
+
line-height: 26px;
|
38
39
|
}
|
39
40
|
ul.features {
|
40
41
|
margin-bottom: 12px;
|
@@ -112,6 +112,21 @@ module Koudoku::Subscription
|
|
112
112
|
end
|
113
113
|
|
114
114
|
finalize_plan_change!
|
115
|
+
|
116
|
+
# if they're updating their credit card details.
|
117
|
+
elsif self.credit_card_token.present?
|
118
|
+
|
119
|
+
prepare_for_card_update
|
120
|
+
|
121
|
+
# fetch the customer.
|
122
|
+
customer = Stripe::Customer.retrieve(self.stripe_id)
|
123
|
+
customer.card = self.credit_card_token
|
124
|
+
customer.save
|
125
|
+
|
126
|
+
# update the last four based on this new card.
|
127
|
+
self.last_four = customer.active_card.last4
|
128
|
+
|
129
|
+
finalize_card_update!
|
115
130
|
|
116
131
|
end
|
117
132
|
|
@@ -184,6 +199,9 @@ module Koudoku::Subscription
|
|
184
199
|
|
185
200
|
def prepare_for_cancelation
|
186
201
|
end
|
202
|
+
|
203
|
+
def prepare_for_card_update
|
204
|
+
end
|
187
205
|
|
188
206
|
def finalize_plan_change!
|
189
207
|
end
|
@@ -200,6 +218,9 @@ module Koudoku::Subscription
|
|
200
218
|
def finalize_cancelation!
|
201
219
|
end
|
202
220
|
|
221
|
+
def finalize_card_update!
|
222
|
+
end
|
223
|
+
|
203
224
|
def card_was_declined
|
204
225
|
end
|
205
226
|
|
@@ -212,7 +233,7 @@ module Koudoku::Subscription
|
|
212
233
|
|
213
234
|
def charge_disputed
|
214
235
|
end
|
215
|
-
|
236
|
+
|
216
237
|
end
|
217
238
|
|
218
239
|
end
|
@@ -1,34 +1,87 @@
|
|
1
1
|
module Koudoku
|
2
2
|
class SubscriptionsController < ApplicationController
|
3
3
|
before_filter :load_owner
|
4
|
-
before_filter :show_existing_subscription, only: [:index, :new, :create]
|
4
|
+
before_filter :show_existing_subscription, only: [:index, :new, :create], unless: :no_owner?
|
5
5
|
before_filter :load_subscription, only: [:show, :cancel, :edit, :update]
|
6
6
|
before_filter :load_plans, only: [:index, :edit]
|
7
|
+
|
7
8
|
def load_plans
|
8
9
|
@plans = ::Plan.order(:price)
|
9
10
|
end
|
11
|
+
|
10
12
|
def load_owner
|
11
13
|
# e.g. User.find(params[:user_id])
|
12
|
-
@owner = Koudoku.owner_class.find(params[:owner_id])
|
14
|
+
@owner = Koudoku.owner_class.find(params[:owner_id]) unless params[:owner_id].nil?
|
13
15
|
end
|
16
|
+
|
17
|
+
def no_owner?
|
18
|
+
@owner.nil?
|
19
|
+
end
|
20
|
+
|
14
21
|
def load_subscription
|
15
22
|
@subscription = ::Subscription.find(params[:id])
|
16
23
|
end
|
24
|
+
|
25
|
+
# the following two methods allow us to show the pricing table before someone has an account.
|
26
|
+
# by default these support devise, but they can be overriden to support others.
|
27
|
+
def current_owner
|
28
|
+
# e.g. "self.current_user"
|
29
|
+
send "current_#{Koudoku.subscriptions_owned_by.to_s}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def redirect_to_sign_up
|
33
|
+
session["#{Koudoku.subscriptions_owned_by.to_s}_return_to"] = new_subscription_path(plan: params[:plan])
|
34
|
+
redirect_to new_registration_path(Koudoku.subscriptions_owned_by.to_s)
|
35
|
+
end
|
36
|
+
|
17
37
|
def index
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
38
|
+
|
39
|
+
# don't bother showing the index if they've already got a subscription.
|
40
|
+
if current_owner and current_owner.subscription.present?
|
41
|
+
redirect_to koudoku.edit_owner_subscription_path(current_owner, current_owner.subscription)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load all plans.
|
45
|
+
@plans = ::Plan.order(:display_order).all
|
46
|
+
|
47
|
+
# Don't prep a subscription unless a user is authenticated.
|
48
|
+
unless no_owner?
|
49
|
+
# we should also set the owner of the subscription here.
|
50
|
+
@subscription = ::Subscription.new({Koudoku.owner_id_sym => @owner.id})
|
51
|
+
# e.g. @subscription.user = @owner
|
52
|
+
@subscription.send Koudoku.owner_assignment_sym, @owner
|
53
|
+
end
|
54
|
+
|
22
55
|
end
|
56
|
+
|
23
57
|
def new
|
24
|
-
|
25
|
-
|
58
|
+
if no_owner?
|
59
|
+
|
60
|
+
if defined?(Devise)
|
61
|
+
|
62
|
+
# by default these methods support devise.
|
63
|
+
if current_owner
|
64
|
+
redirect_to new_owner_subscription_path(current_owner, plan: params[:plan])
|
65
|
+
else
|
66
|
+
redirect_to_sign_up
|
67
|
+
end
|
68
|
+
|
69
|
+
else
|
70
|
+
raise "This feature depends on Devise for authentication."
|
71
|
+
end
|
72
|
+
|
73
|
+
else
|
74
|
+
@subscription = ::Subscription.new
|
75
|
+
@subscription.plan = ::Plan.find(params[:plan])
|
76
|
+
end
|
26
77
|
end
|
78
|
+
|
27
79
|
def show_existing_subscription
|
28
80
|
if @owner.subscription.present?
|
29
81
|
redirect_to owner_subscription_path(@owner, @owner.subscription)
|
30
82
|
end
|
31
83
|
end
|
84
|
+
|
32
85
|
def create
|
33
86
|
@subscription = ::Subscription.new(params[:subscription])
|
34
87
|
@subscription.user = @owner
|
@@ -40,16 +93,20 @@ module Koudoku
|
|
40
93
|
render :new
|
41
94
|
end
|
42
95
|
end
|
96
|
+
|
43
97
|
def show
|
44
98
|
end
|
99
|
+
|
45
100
|
def cancel
|
46
101
|
flash[:success] = "You've successfully cancelled your subscription."
|
47
102
|
@subscription.plan_id = nil
|
48
103
|
@subscription.save
|
49
104
|
redirect_to owner_subscription_path(@owner, @subscription)
|
50
105
|
end
|
106
|
+
|
51
107
|
def edit
|
52
108
|
end
|
109
|
+
|
53
110
|
def update
|
54
111
|
if @subscription.update_attributes(params[:subscription])
|
55
112
|
flash[:success] = "You've been successfully upgraded."
|
@@ -59,5 +116,6 @@ module Koudoku
|
|
59
116
|
render :edit
|
60
117
|
end
|
61
118
|
end
|
119
|
+
|
62
120
|
end
|
63
121
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
<script type="text/javascript" src="https://js.stripe.com/v1/"></script>
|
2
|
+
|
3
|
+
<%= form_for @subscription, url: url, html: {id: 'payment-form', class: 'form-horizontal'} do |f| %>
|
4
|
+
|
5
|
+
<fieldset>
|
6
|
+
|
7
|
+
<legend><%= title %></legend>
|
8
|
+
|
9
|
+
<div class="control-group">
|
10
|
+
<label class="control-label">Card Number</label>
|
11
|
+
<div class="controls">
|
12
|
+
<input type="text" size="20" autocomplete="off" class="card-number"/>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="control-group">
|
17
|
+
<label class="control-label">Expiration (MM/YYYY)</label>
|
18
|
+
<div class="controls">
|
19
|
+
<input type="text" size="2" class="card-expiry-month input-mini"/>
|
20
|
+
<span> / </span>
|
21
|
+
<input type="text" size="4" class="card-expiry-year input-mini"/>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="control-group">
|
26
|
+
<label class="control-label">CVC</label>
|
27
|
+
<div class="controls">
|
28
|
+
<input type="text" size="4" autocomplete="off" class="card-cvc input-small"/>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="alert alert-error payment-errors"></div>
|
33
|
+
<%= f.hidden_field :plan_id %>
|
34
|
+
|
35
|
+
</fieldset>
|
36
|
+
|
37
|
+
<div class="control-group">
|
38
|
+
<div class="controls">
|
39
|
+
<% if Koudoku.free_trial? %>
|
40
|
+
<button type="submit" class="btn btn-primary submit-button">Save Billing Information</button>
|
41
|
+
<% else %>
|
42
|
+
<button type="submit" class="btn btn-primary submit-button">Upgrade Your Account</button>
|
43
|
+
<% end %>
|
44
|
+
<%= link_to "Cancel", owner_subscriptions_path(@owner), class: 'btn' %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<% end %>
|
49
|
+
|
50
|
+
<script type="text/javascript">
|
51
|
+
|
52
|
+
// All this code taken from Stripe's own examples at:
|
53
|
+
// https://stripe.com/docs/tutorials/forms .
|
54
|
+
|
55
|
+
Stripe.setPublishableKey("<%= Koudoku.stripe_publishable_key %>");
|
56
|
+
|
57
|
+
function stripeResponseHandler(status, response) {
|
58
|
+
|
59
|
+
if (response.error) {
|
60
|
+
// show the errors on the form
|
61
|
+
$(".payment-errors").text(response.error.message).show();
|
62
|
+
$(".submit-button").removeAttr("disabled");
|
63
|
+
} else {
|
64
|
+
var form$ = $("#payment-form");
|
65
|
+
// token contains id, last4, and card type
|
66
|
+
// insert the token into the form so it gets submitted to the server
|
67
|
+
form$.append("<input type='hidden' name='subscription[credit_card_token]' value='" + response['id'] + "'/>");
|
68
|
+
form$.append("<input type='hidden' name='subscription[last_four]' value='" + response['last4'] + "'/>");
|
69
|
+
form$.append("<input type='hidden' name='subscription[card_type]' value='" + response['card_type'] + "'/>");
|
70
|
+
// and submit
|
71
|
+
form$.get(0).submit();
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
$(document).ready(function() {
|
76
|
+
|
77
|
+
// By default, don't show errors.
|
78
|
+
$(".payment-errors").hide()
|
79
|
+
|
80
|
+
$("#payment-form").submit(function(event) {
|
81
|
+
|
82
|
+
// disable the submit button to prevent repeated clicks
|
83
|
+
$('.submit-button').attr("disabled", "disabled");
|
84
|
+
|
85
|
+
Stripe.createToken({
|
86
|
+
number: $('.card-number').val(),
|
87
|
+
cvc: $('.card-cvc').val(),
|
88
|
+
exp_month: $('.card-expiry-month').val(),
|
89
|
+
exp_year: $('.card-expiry-year').val()
|
90
|
+
}, stripeResponseHandler);
|
91
|
+
|
92
|
+
// prevent the form from submitting with the default action
|
93
|
+
return false;
|
94
|
+
});
|
95
|
+
});
|
96
|
+
|
97
|
+
</script>
|
@@ -1,12 +1,14 @@
|
|
1
1
|
<ul class="thumbnails koudoku-pricing-table">
|
2
2
|
<% @plans.each do |plan| %>
|
3
|
-
<li class="span3 plan <%= 'plan-primary' if plan.
|
3
|
+
<li class="span3 plan <%= 'plan-primary' if plan.highlight? %>">
|
4
4
|
<div class="thumbnail">
|
5
5
|
<div class="caption">
|
6
|
-
<
|
7
|
-
<
|
6
|
+
<h3><%= plan.name %></h3>
|
7
|
+
<h4><%= number_to_currency(plan.price) %>/month</h4>
|
8
8
|
<div class="call-to-action">
|
9
|
-
<% if @subscription.
|
9
|
+
<% if @subscription.nil? %>
|
10
|
+
<%= link_to Koudoku.free_trial? ? 'Start Trial' : 'Sign Up', koudoku.new_subscription_path(plan: plan.id), class: "btn btn-success btn-large" %>
|
11
|
+
<% elsif @subscription.persisted? %>
|
10
12
|
<% if @subscription.plan == plan %>
|
11
13
|
<%= form_for @subscription, url: owner_subscription_path(@owner, @subscription) do |f| %>
|
12
14
|
<%= f.submit 'Selected', class: "btn btn-large", disabled: 'disabled' %>
|
@@ -25,10 +27,7 @@
|
|
25
27
|
<% if Koudoku.free_trial? %>
|
26
28
|
<li class='muted'><%= Koudoku.free_trial_length %>-day Free Trial</li>
|
27
29
|
<% end %>
|
28
|
-
|
29
|
-
<li>Testing.</li>
|
30
|
-
<li>Testing.</li>
|
31
|
-
<li>Testing.</li>
|
30
|
+
<%= BlueCloth.new(plan.features.gsub(/\n/, "\n\n")).to_html.gsub(/<(\/?)p>/, '<\1li>').html_safe %>
|
32
31
|
</ul>
|
33
32
|
</div>
|
34
33
|
</div>
|
@@ -36,18 +35,6 @@
|
|
36
35
|
<% end %>
|
37
36
|
|
38
37
|
<li class="span3 social-proof">
|
39
|
-
|
40
|
-
<blockquote>
|
41
|
-
<h3>"Some Great Quote"</h3> — Person at Place
|
42
|
-
</blockquote>
|
43
|
-
|
44
|
-
<blockquote>
|
45
|
-
<h4>"This is a great service and you've been the best ever! Thanks!"</h4> — Person at Place
|
46
|
-
</blockquote>
|
47
|
-
|
48
|
-
<blockquote>
|
49
|
-
<h4>"I love you guys and you're the best! You're the best!"</h4> — Person at Place
|
50
|
-
</blockquote>
|
51
|
-
|
38
|
+
<%= render 'koudoku/subscriptions/social_proof' %>
|
52
39
|
</li>
|
53
40
|
</ul>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<blockquote>
|
2
|
+
<h4>"Some Great Quote"</h4> — Person at Place
|
3
|
+
</blockquote>
|
4
|
+
|
5
|
+
<blockquote>
|
6
|
+
<h4>"This is a great service and you've been the best ever! Thanks!"</h4> — Person at Place
|
7
|
+
</blockquote>
|
8
|
+
|
9
|
+
<blockquote>
|
10
|
+
<h4>"I love you guys and you're the best! You're the best!"</h4> — Person at Place
|
11
|
+
</blockquote>
|
File without changes
|
@@ -1,3 +1,7 @@
|
|
1
|
-
|
2
|
-
<%= render '
|
3
|
-
|
1
|
+
<% if params['update'] == 'card' %>
|
2
|
+
<%= render 'card', title: "Update Your Payment Information", url: owner_subscription_path(@owner, @subscription) %>
|
3
|
+
<% else %>
|
4
|
+
<h1>What Plan Is Best For You?</h1>
|
5
|
+
<%= render 'pricing_table' %>
|
6
|
+
<p>You can also <%= link_to 'cancel your subscription', cancel_owner_subscription_path(@owner, @subscription), method: :post %>.</p>
|
7
|
+
<% end %>
|
@@ -1,99 +1 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
<%= form_for @subscription, url: @subscription.persisted? ? owner_subscription_path(@owner, @subscription) : owner_subscriptions_path(@owner), html: {id: 'payment-form', class: 'form-horizontal'} do |f| %>
|
4
|
-
<% if Koudoku.free_trial? %>
|
5
|
-
<h1>Start Your Free Trial</h1>
|
6
|
-
<% else %>
|
7
|
-
<h1>Upgrade Your Account</h1>
|
8
|
-
<% end %>
|
9
|
-
|
10
|
-
<fieldset>
|
11
|
-
|
12
|
-
<div class="alert alert-error payment-errors"></div>
|
13
|
-
|
14
|
-
<%= f.hidden_field :plan_id %>
|
15
|
-
|
16
|
-
<div class="control-group">
|
17
|
-
<label class="control-label">Card Number</label>
|
18
|
-
<div class="controls">
|
19
|
-
<input type="text" size="20" autocomplete="off" class="card-number"/>
|
20
|
-
</div>
|
21
|
-
</div>
|
22
|
-
|
23
|
-
<div class="control-group">
|
24
|
-
<label class="control-label">Expiration (MM/YYYY)</label>
|
25
|
-
<div class="controls">
|
26
|
-
<input type="text" size="2" class="card-expiry-month input-mini"/>
|
27
|
-
<span> / </span>
|
28
|
-
<input type="text" size="4" class="card-expiry-year input-mini"/>
|
29
|
-
</div>
|
30
|
-
</div>
|
31
|
-
|
32
|
-
<div class="control-group">
|
33
|
-
<label class="control-label">CVC</label>
|
34
|
-
<div class="controls">
|
35
|
-
<input type="text" size="4" autocomplete="off" class="card-cvc input-small"/>
|
36
|
-
</div>
|
37
|
-
</div>
|
38
|
-
|
39
|
-
</fieldset>
|
40
|
-
|
41
|
-
<div class="form-actions">
|
42
|
-
<% if Koudoku.free_trial? %>
|
43
|
-
<button type="submit" class="btn btn-primary submit-button">Save Billing Information</button>
|
44
|
-
<% else %>
|
45
|
-
<button type="submit" class="btn btn-primary submit-button">Upgrade Your Account</button>
|
46
|
-
<% end %>
|
47
|
-
<%= link_to "Cancel", owner_subscriptions_path(@owner), class: 'btn' %>
|
48
|
-
</div>
|
49
|
-
|
50
|
-
<% end %>
|
51
|
-
|
52
|
-
<script type="text/javascript">
|
53
|
-
|
54
|
-
// All this code taken from Stripe's own examples at:
|
55
|
-
// https://stripe.com/docs/tutorials/forms .
|
56
|
-
|
57
|
-
Stripe.setPublishableKey("<%= Koudoku.stripe_publishable_key %>");
|
58
|
-
|
59
|
-
function stripeResponseHandler(status, response) {
|
60
|
-
|
61
|
-
if (response.error) {
|
62
|
-
// show the errors on the form
|
63
|
-
$(".payment-errors").text(response.error.message).show();
|
64
|
-
$(".submit-button").removeAttr("disabled");
|
65
|
-
} else {
|
66
|
-
var form$ = $("#payment-form");
|
67
|
-
// token contains id, last4, and card type
|
68
|
-
// insert the token into the form so it gets submitted to the server
|
69
|
-
form$.append("<input type='hidden' name='subscription[credit_card_token]' value='" + response['id'] + "'/>");
|
70
|
-
form$.append("<input type='hidden' name='subscription[last_four]' value='" + response['last4'] + "'/>");
|
71
|
-
form$.append("<input type='hidden' name='subscription[card_type]' value='" + response['card_type'] + "'/>");
|
72
|
-
// and submit
|
73
|
-
form$.get(0).submit();
|
74
|
-
}
|
75
|
-
}
|
76
|
-
|
77
|
-
$(document).ready(function() {
|
78
|
-
|
79
|
-
// By default, don't show errors.
|
80
|
-
$(".payment-errors").hide()
|
81
|
-
|
82
|
-
$("#payment-form").submit(function(event) {
|
83
|
-
|
84
|
-
// disable the submit button to prevent repeated clicks
|
85
|
-
$('.submit-button').attr("disabled", "disabled");
|
86
|
-
|
87
|
-
Stripe.createToken({
|
88
|
-
number: $('.card-number').val(),
|
89
|
-
cvc: $('.card-cvc').val(),
|
90
|
-
exp_month: $('.card-expiry-month').val(),
|
91
|
-
exp_year: $('.card-expiry-year').val()
|
92
|
-
}, stripeResponseHandler);
|
93
|
-
|
94
|
-
// prevent the form from submitting with the default action
|
95
|
-
return false;
|
96
|
-
});
|
97
|
-
});
|
98
|
-
|
99
|
-
</script>
|
1
|
+
<%= render 'card', title: (Koudoku.free_trial? ? "Start Your Free Trial" : "Upgrade Your Account"), url: owner_subscriptions_path(@owner) %>
|
@@ -1,8 +1,15 @@
|
|
1
|
-
<h1>Thanks!</h1>
|
2
1
|
<% if @subscription.plan.present? %>
|
3
|
-
<
|
4
|
-
|
2
|
+
<h2>You're Subscribed!</h2>
|
3
|
+
<p>Your currently subscribed to the <%= @subscription.plan.name %> plan.</p>
|
4
|
+
<%= link_to 'Choose Another Plan', edit_owner_subscription_path(@owner, @subscription), class: 'btn' %>
|
5
5
|
<% else %>
|
6
|
+
<h2>No Subscription</h2>
|
6
7
|
<p>You are not subscribed to a paid plan.</p>
|
7
|
-
<%= link_to 'Choose
|
8
|
-
<% end %>
|
8
|
+
<%= link_to 'Choose A Plan', edit_owner_subscription_path(@owner, @subscription), class: 'btn' %>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<br><br>
|
12
|
+
|
13
|
+
<h4>Payment Information</h4>
|
14
|
+
<p>The card on file for your account ends with <%= @subscription.last_four %>.</p>
|
15
|
+
<%= link_to 'Update Payment Information', edit_owner_subscription_path(@owner, @subscription, update: 'card'), class: 'btn' %>
|
data/config/routes.rb
CHANGED
@@ -10,7 +10,7 @@ module Koudoku
|
|
10
10
|
class InstallGenerator < Rails::Generators::Base
|
11
11
|
|
12
12
|
# Not sure what this does.
|
13
|
-
source_root File.expand_path("
|
13
|
+
source_root File.expand_path("../../../../app/views/koudoku/subscriptions", __FILE__)
|
14
14
|
|
15
15
|
include Rails::Generators::Migration
|
16
16
|
|
@@ -42,7 +42,7 @@ RUBY
|
|
42
42
|
gsub_file "app/models/subscription.rb", /ActiveRecord::Base/, "ActiveRecord::Base\n include Koudoku::Subscription\n\n belongs_to :#{subscription_owner_model}\n belongs_to :coupon\n"
|
43
43
|
|
44
44
|
# Add the plans.
|
45
|
-
generate("model", "plan name:string stripe_id:string price:float")
|
45
|
+
generate("model", "plan name:string stripe_id:string price:float features:text highlight:boolean display_order:integer")
|
46
46
|
gsub_file "app/models/plan.rb", /ActiveRecord::Base/, "ActiveRecord::Base\n include Koudoku::Plan\n belongs_to :#{subscription_owner_model}\n belongs_to :coupon\n has_many :subscriptions\n"
|
47
47
|
|
48
48
|
# Add coupons.
|
@@ -54,9 +54,14 @@ RUBY
|
|
54
54
|
|
55
55
|
# Update the owner relationship.
|
56
56
|
gsub_file "app/models/#{subscription_owner_model}.rb", /ActiveRecord::Base/, "ActiveRecord::Base\n\n # Added by Koudoku.\n has_one :subscription\n\n"
|
57
|
+
|
58
|
+
# Install the pricing table.
|
59
|
+
["_social_proof.html.erb"].each do |file|
|
60
|
+
copy_file "#{}file", "app/views/koudoku/subscriptions/#{file}"
|
61
|
+
end
|
57
62
|
|
58
63
|
# Add webhooks to the route.
|
59
|
-
gsub_file "config/routes.rb", /Application.routes.draw do/, "Application.routes.draw do\n\n # Added by Koudoku.\n mount Koudoku::Engine, at: \
|
64
|
+
gsub_file "config/routes.rb", /Application.routes.draw do/, "Application.routes.draw do\n\n # Added by Koudoku.\n mount Koudoku::Engine, at: 'koudoku'\n match 'pricing' => 'koudoku::subscriptions#index', as: 'pricing'\n\n"
|
60
65
|
|
61
66
|
# Show the user the API key we generated.
|
62
67
|
say "\nTo enable support for Stripe webhooks, point it to \"/koudoku/webhooks?api_key=#{api_key}\". This API key has been randomly generated, so it's unique to your application.\n\n"
|
data/lib/koudoku/engine.rb
CHANGED
data/lib/koudoku/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: koudoku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 3.2.
|
21
|
+
version: 3.2.11
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 3.2.
|
29
|
+
version: 3.2.11
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: stripe
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: bluecloth
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: sqlite3
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -76,7 +92,10 @@ files:
|
|
76
92
|
- app/controllers/koudoku/subscriptions_controller.rb
|
77
93
|
- app/controllers/koudoku/webhooks_controller.rb
|
78
94
|
- app/helpers/koudoku/application_helper.rb
|
95
|
+
- app/views/koudoku/subscriptions/_card.html.erb
|
79
96
|
- app/views/koudoku/subscriptions/_pricing_table.html.erb
|
97
|
+
- app/views/koudoku/subscriptions/_social_proof.html.erb
|
98
|
+
- app/views/koudoku/subscriptions/_stripe_js.html.erb
|
80
99
|
- app/views/koudoku/subscriptions/edit.html.erb
|
81
100
|
- app/views/koudoku/subscriptions/index.html.erb
|
82
101
|
- app/views/koudoku/subscriptions/new.html.erb
|
@@ -137,7 +156,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
156
|
version: '0'
|
138
157
|
segments:
|
139
158
|
- 0
|
140
|
-
hash:
|
159
|
+
hash: 1525558269552426845
|
141
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
161
|
none: false
|
143
162
|
requirements:
|
@@ -146,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
165
|
version: '0'
|
147
166
|
segments:
|
148
167
|
- 0
|
149
|
-
hash:
|
168
|
+
hash: 1525558269552426845
|
150
169
|
requirements: []
|
151
170
|
rubyforge_project:
|
152
171
|
rubygems_version: 1.8.24
|