kern 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +13 -2
- data/README.md +15 -13
- data/app/assets/builds/tailwind.css +1 -1
- data/app/controllers/kern/billing_profiles/subscriptions_controller.rb +37 -0
- data/app/controllers/kern/billing_profiles/subscriptions_controller.rb.tt +37 -0
- data/app/controllers/kern/settings/subscriptions_controller.rb +6 -0
- data/app/controllers/kern/signups_controller.rb +1 -1
- data/app/models/billing_profile/subscription.rb +16 -0
- data/app/models/billing_profile.rb +7 -0
- data/app/models/session.rb +1 -1
- data/app/models/signup.rb +1 -1
- data/app/models/user.rb +1 -1
- data/app/models/workspace/access.rb +3 -0
- data/app/models/workspace/access.rb.tt +10 -0
- data/app/models/workspace/billable.rb +16 -0
- data/app/models/workspace/feature_access.rb +28 -0
- data/app/models/workspace/members.rb +9 -7
- data/app/models/workspace/setup.rb +5 -3
- data/app/models/workspace.rb +2 -0
- data/app/views/components/_dialog.html.erb +36 -0
- data/app/views/kern/pages/welcome.html.erb +10 -6
- data/app/views/kern/settings/_cards.html.erb +11 -3
- data/app/views/kern/settings/subscriptions/_plan.html.erb +35 -0
- data/app/views/kern/settings/subscriptions/show.html.erb +11 -0
- data/app/views/kern/settings/users/show.html.erb +2 -2
- data/app/views/kern/signups/new.html.erb +2 -0
- data/app/views/layouts/kern/application.html.erb +3 -1
- data/app/views/layouts/kern/application.html.erb.tt +1 -0
- data/app/views/layouts/kern/auth.html.erb +1 -1
- data/app/webhooks/stripe/base.rb +31 -0
- data/app/webhooks/stripe/checkout_session_completed.rb +46 -0
- data/app/webhooks/stripe/customer_subscription_deleted.rb +20 -0
- data/app/webhooks/stripe/customer_subscription_updated.rb +33 -0
- data/bin/release +2 -3
- data/config/initializers/stripe.rb +5 -0
- data/config/initializers/stripe.rb.tt +3 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20250101000007_create_billing_profiles.rb +14 -0
- data/db/migrate/20250101000008_create_billing_profile_subscriptions.rb +20 -0
- data/lib/generators/kern/feature/USAGE +1 -0
- data/lib/generators/kern/feature/feature_generator.rb +82 -19
- data/lib/generators/kern/install/install_generator.rb +43 -7
- data/lib/generators/kern/install/templates/POST_INSTALL +25 -0
- data/lib/generators/kern/install/templates/configurations/plans.yml +43 -0
- data/lib/generators/kern/install/templates/configurations/stripe.yml +11 -0
- data/lib/generators/kern/layouts/layouts_generator.rb +12 -0
- data/lib/kern/version.rb +1 -1
- metadata +25 -3
- data/lib/generators/kern/views/USAGE +0 -22
- data/lib/generators/kern/views/views_generator.rb +0 -42
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
</h1>
|
|
5
5
|
|
|
6
6
|
<p class="mt-2 text-sm md:text-base text-gray-800">
|
|
7
|
-
SaaS foundation for Ruby on Rails apps by <%= link_to "Rails Designer", "https://railsdesigner.com" %>. You are ready to build your next SaaS.
|
|
7
|
+
SaaS foundation for Ruby on Rails apps by <%= link_to "Rails Designer", "https://railsdesigner.com" %>. You are ready to build your next SaaS. 🎉
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<section class="mt-6">
|
|
@@ -12,21 +12,25 @@
|
|
|
12
12
|
Get started
|
|
13
13
|
</h2>
|
|
14
14
|
|
|
15
|
-
<ul class="list-disc list-inside marker:text-blue-500">
|
|
15
|
+
<ul class="list-disc list-inside marker:text-blue-500 [&_code]:font-bold">
|
|
16
16
|
<li>
|
|
17
|
-
add your own root route (<code>root to: "dashboard#show"</code>)
|
|
17
|
+
add your own root route (<code class="select-all">root to: "dashboard#show"</code>)
|
|
18
18
|
</li>
|
|
19
19
|
|
|
20
20
|
<li>
|
|
21
|
-
install <%= link_to "Rails Icons", "https://github.com/Rails-Designer/rails_icons" %>; Kern uses the <b>Phosphor</b> library
|
|
21
|
+
install <%= link_to "Rails Icons", "https://github.com/Rails-Designer/rails_icons" %>; Kern uses the <b>Phosphor</b> library (but you can choose any other library)
|
|
22
22
|
</li>
|
|
23
23
|
|
|
24
24
|
<li>
|
|
25
|
-
override any views by running <code>bin/rails g kern:views</code>
|
|
25
|
+
override any views by running <code class="select-all">bin/rails g kern:views</code>
|
|
26
26
|
</li>
|
|
27
27
|
|
|
28
28
|
<li>
|
|
29
|
-
override any feature by running <code>bin/rals g kern:feature</code>
|
|
29
|
+
override any feature by running <code class="select-all">bin/rals g kern:feature</code>
|
|
30
|
+
</li>
|
|
31
|
+
|
|
32
|
+
<li>
|
|
33
|
+
check out <%= link_to "Rails Designer's UI Components", "https://railsdesigner.com/components/" %>
|
|
30
34
|
</li>
|
|
31
35
|
|
|
32
36
|
<li>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
<%# locals: (card_css: "col-span-12 md:col-span-4 lg:col-span-3", link_css: "block px-3 py-2 border border-gray-100 rounded-md transition ease-in-out delay-75 hover:border-gray-200
|
|
2
|
-
<ul class="grid grid-cols-12">
|
|
1
|
+
<%# locals: (card_css: "col-span-12 md:col-span-4 lg:col-span-3", link_css: "block px-3 py-2 border border-gray-100 rounded-md transition ease-in-out delay-75 hover:border-gray-200", heading_css: "text-base font-bold tracking-tight text-gray-800", description_css: "mt-0.5 text-sm font-light text-gray-600") %>
|
|
2
|
+
<ul class="grid grid-cols-12 gap-8">
|
|
3
3
|
<%= tag.li class: card_css do %>
|
|
4
4
|
<%= link_to settings_user_path, class: link_css do %>
|
|
5
|
-
<%= tag.h4 "
|
|
5
|
+
<%= tag.h4 "User settings", class: heading_css %>
|
|
6
6
|
|
|
7
7
|
<%= tag.p "Update your email and password", class: description_css %>
|
|
8
8
|
<% end %>
|
|
9
9
|
<% end %>
|
|
10
|
+
|
|
11
|
+
<%= tag.li class: card_css do %>
|
|
12
|
+
<%= link_to settings_subscriptions_path, class: link_css do %>
|
|
13
|
+
<%= tag.h4 "Subscription", class: heading_css %>
|
|
14
|
+
|
|
15
|
+
<%= tag.p "Edit subscription, manage billing and invoices", class: description_css %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
10
18
|
</ul>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%# locals: (plan:) %>
|
|
2
|
+
<li class="col-span-12 flex flex-col justify-between bg-white ring ring-gray-200/60 rounded-md md:col-span-4">
|
|
3
|
+
<div>
|
|
4
|
+
<div class="px-4 py-3 bg-gray-50">
|
|
5
|
+
<h3 class="text-sm font-medium tracking-tight text-gray-800 md:text-base">
|
|
6
|
+
<%= plan[:name] %>
|
|
7
|
+
</h3>
|
|
8
|
+
|
|
9
|
+
<p class="mt-1">
|
|
10
|
+
<span class="text-lg font-light text-gray-800">$</span>
|
|
11
|
+
<span class="text-3xl font-semibold text-gray-800"><%= plan[:amount] %></span>
|
|
12
|
+
<span class="text-base font-light text-gray-800">/month</span>
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<ul class="grid gap-2 grow-1 px-4 py-4 bg-white border-t border-gray-100">
|
|
17
|
+
<% plan[:features].each do |key, value| %>
|
|
18
|
+
<% next if value == false %>
|
|
19
|
+
|
|
20
|
+
<li class="flex items-center gap-x-1.5 text-sm font-medium text-gray-800">
|
|
21
|
+
<%= icon "check", class: "size-3 text-brand-600 shrink-0" rescue nil %>
|
|
22
|
+
|
|
23
|
+
<% if value == true %>
|
|
24
|
+
<%= key.to_s.humanize %>
|
|
25
|
+
<% else %>
|
|
26
|
+
<% label = key.to_s.gsub('_count', '').humanize.downcase %>
|
|
27
|
+
<%= pluralize(number_with_delimiter(value), label) %>
|
|
28
|
+
<% end %>
|
|
29
|
+
</li>
|
|
30
|
+
<% end %>
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<%= button_to "Subscribe", subscriptions_path, form_class: "flex justify-center px-4 py-3 border-t border-gray-100", params: {price_id: plan[:price_id]}, class: "btn-primary btn-block" %>
|
|
35
|
+
</li>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "Subscription" } %>
|
|
2
|
+
|
|
3
|
+
<%= component "container" do %>
|
|
4
|
+
<%= tag.ul class: "grid grid-cols-12 gap-4 w-full my-20 md:gap-8" do %>
|
|
5
|
+
<% Config::Plans.except(:default).values.each do |plan| %>
|
|
6
|
+
<%= render "plan", plan: plan %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end unless Current.workspace.subscribed? %>
|
|
9
|
+
|
|
10
|
+
<%= button_to "Go to billing", edit_subscriptions_path, method: :get, data: {turbo: false}, class: "btn-secondary" if Current.workspace.subscribed? %>
|
|
11
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<% content_for :title, "
|
|
1
|
+
<% content_for :title, "User settings" %>
|
|
2
2
|
|
|
3
|
-
<%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "
|
|
3
|
+
<%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "User" } %>
|
|
4
4
|
|
|
5
5
|
<%= component "container" do %>
|
|
6
6
|
<%= form_with model: [:settings, @user], class: "max-w-lg" do |form| %>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<% content_for :title, "Create your account" %>
|
|
2
2
|
<% content_for :page_title, "Create your account" %>
|
|
3
3
|
|
|
4
|
+
<%= tag.p "Configure Active Record Encryption before continuing", class: "mx-4 px-2 py-1 text-sm font-medium text-center text-red-900 bg-red-50 border border-red-100 rounded-md" if Rails.env.development? && Rails.application.credentials.active_record_encryption&.primary_key.blank? %>
|
|
5
|
+
|
|
4
6
|
<%= form_with model: @signup, url: signup_path, class: "px-2 md:px-4" do |form| %>
|
|
5
7
|
<%= form.input :email_address %>
|
|
6
8
|
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
<%= csp_meta_tag %>
|
|
10
10
|
|
|
11
11
|
<%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
|
|
12
|
+
<script defer src="https://cdn.jsdelivr.net/npm/attractivejs@0.9.0"></script>
|
|
12
13
|
</head>
|
|
13
14
|
|
|
14
|
-
<body class="<%= class_names("antialiased bg-white selection:bg-
|
|
15
|
+
<body class="<%= class_names("antialiased bg-white selection:bg-blue-200/40 selection:text-blue-800/70", yield(:body_class)) %>">
|
|
15
16
|
<div class="flex">
|
|
16
17
|
<%= render partial: "layouts/kern/application/navigation",
|
|
17
18
|
locals: {
|
|
@@ -27,5 +28,6 @@
|
|
|
27
28
|
</div>
|
|
28
29
|
|
|
29
30
|
<%= component "flash" %>
|
|
31
|
+
<%= component "dialog" %>
|
|
30
32
|
</body>
|
|
31
33
|
</html>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<%%= csp_meta_tag %>
|
|
10
10
|
|
|
11
11
|
<%%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
|
|
12
|
+
<script defer src="https://cdn.jsdelivr.net/npm/attractivejs@0.9.0"></script>
|
|
12
13
|
</head>
|
|
13
14
|
|
|
14
15
|
<body class="<%%= class_names("antialiased bg-white selection:bg-pink-200/40 selection:text-pink-800/70", yield(:body_class)) %>">
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
|
|
13
13
|
</head>
|
|
14
14
|
|
|
15
|
-
<body class="relative antialiased
|
|
15
|
+
<body class="relative overflow-clip antialiased bg-white selection:bg-blue-200/40 selection:text-blue-800/70">
|
|
16
16
|
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="absolute -top-[400px] -left-[350px] size-[800px] text-brand-500">
|
|
17
17
|
<path fill="currentColor" d="M42.9,-43.4C60.9,-36,84.8,-27.6,92.2,-12.2C99.8,3.1,91.2,25.4,76.1,37C61.1,48.5,39.7,49.3,21.9,52.7C4,56.1,-10.4,62,-26.6,61.1C-42.9,60.3,-61.1,52.9,-61.9,41C-62.6,28.8,-46,12.1,-42.2,-7C-38.4,-25.9,-47.4,-46.5,-42.5,-56.1C-37.6,-65.8,-18.8,-64.5,-3.2,-60.7C12.4,-57,24.9,-50.8,42.9,-43.4Z" transform="translate(100 100)" />
|
|
18
18
|
</svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Stripe
|
|
2
|
+
class Base < Fuik::Event
|
|
3
|
+
def self.verify!(request)
|
|
4
|
+
secret = Rails.application.credentials.dig(:stripe, :signing_secret)
|
|
5
|
+
signature = request.headers["Stripe-Signature"]
|
|
6
|
+
|
|
7
|
+
Stripe::Webhook.construct_event(
|
|
8
|
+
request.raw_post,
|
|
9
|
+
signature,
|
|
10
|
+
secret
|
|
11
|
+
)
|
|
12
|
+
rescue JSON::ParserError, Stripe::SignatureVerificationError => error
|
|
13
|
+
raise Fuik::InvalidSignature, error.message
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def subscription_resource
|
|
19
|
+
@subscription_resource ||= ::Stripe::Subscription.retrieve(
|
|
20
|
+
payload["data"]["object"]["id"],
|
|
21
|
+
expand: ["items.data.price"]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def workspace = billing_profile.workspace
|
|
26
|
+
|
|
27
|
+
def billing_profile
|
|
28
|
+
BillingProfile.find_by!(external_id: payload["data"]["object"]["customer"])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Stripe
|
|
2
|
+
class CheckoutSessionCompleted < Base
|
|
3
|
+
def process!
|
|
4
|
+
billing_profile = create_billing_profile_for workspace
|
|
5
|
+
|
|
6
|
+
attach_subscription_items_to billing_profile
|
|
7
|
+
|
|
8
|
+
@webhook_event.processed!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def create_billing_profile_for(workspace)
|
|
14
|
+
BillingProfile.where(
|
|
15
|
+
workspace: workspace,
|
|
16
|
+
external_id: payload["data"]["object"]["customer"]
|
|
17
|
+
).first_or_create
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def attach_subscription_items_to(billing_profile)
|
|
21
|
+
subscription_resource["items"]["data"].each do |item|
|
|
22
|
+
subscription = BillingProfile::Subscription.find_or_initialize_by(
|
|
23
|
+
billing_profile: billing_profile,
|
|
24
|
+
subscription_item_id: item["id"]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
subscription.update!(
|
|
28
|
+
subscription_id: subscription_resource["id"],
|
|
29
|
+
status: subscription_resource["status"],
|
|
30
|
+
cancel_at: subscription_resource["cancel_at"] ? Time.at(subscription_resource["cancel_at"]) : nil,
|
|
31
|
+
current_period_end_at: subscription_resource["current_period_end"] ? Time.at(subscription_resource["current_period_end"]) : nil,
|
|
32
|
+
quantity: item["quantity"],
|
|
33
|
+
product_id: item["price"]["product"],
|
|
34
|
+
price_id: item["price"]["id"],
|
|
35
|
+
interval: item["price"]["recurring"]["interval"]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
workspace.add_access(item["price"]["product"])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def workspace
|
|
43
|
+
@workspace ||= Workspace.sluggable.find(payload["data"]["object"]["client_reference_id"])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Stripe
|
|
2
|
+
class CustomerSubscriptionDeleted < Base
|
|
3
|
+
def process!
|
|
4
|
+
cancel_subscription_items_for billing_profile
|
|
5
|
+
|
|
6
|
+
@webhook_event.processed!
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def cancel_subscription_items_for(billing_profile)
|
|
12
|
+
billing_profile.subscriptions.where(subscription_id: payload["data"]["object"]["id"]).each do |subscription|
|
|
13
|
+
subscription.update!(
|
|
14
|
+
status: "canceled",
|
|
15
|
+
cancel_at: Time.at(payload["data"]["object"]["canceled_at"] || payload["data"]["object"]["ended_at"] || Time.current.to_i)
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Stripe
|
|
2
|
+
class CustomerSubscriptionUpdated < Base
|
|
3
|
+
def process!
|
|
4
|
+
update_subscription_items_for billing_profile
|
|
5
|
+
|
|
6
|
+
@webhook_event.processed!
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def update_subscription_items_for(billing_profile)
|
|
12
|
+
subscription_resource["items"]["data"].each do |item|
|
|
13
|
+
subscription = BillingProfile::Subscription.find_or_initialize_by(
|
|
14
|
+
billing_profile: billing_profile,
|
|
15
|
+
subscription_item_id: item["id"]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
subscription.update!(
|
|
19
|
+
subscription_id: subscription_resource["id"],
|
|
20
|
+
status: subscription_resource["status"],
|
|
21
|
+
cancel_at: subscription_resource["cancel_at"] ? Time.at(subscription_resource["cancel_at"]) : nil,
|
|
22
|
+
current_period_end_at: subscription_resource["current_period_end"] ? Time.at(subscription_resource["current_period_end"]) : nil,
|
|
23
|
+
quantity: item["quantity"],
|
|
24
|
+
product_id: item["price"]["product"],
|
|
25
|
+
price_id: item["price"]["id"],
|
|
26
|
+
interval: item["price"]["recurring"]["interval"]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
workspace.add_access(item["price"]["product"])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/bin/release
CHANGED
|
@@ -9,9 +9,8 @@ if [ -z "$VERSION" ]; then
|
|
|
9
9
|
exit 1
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
|
-
# If `VERSION` is a keyword, bump the current version
|
|
13
12
|
if [[ "$VERSION" =~ ^(major|minor|patch)$ ]]; then
|
|
14
|
-
CURRENT=$(grep -
|
|
13
|
+
CURRENT=$(grep -o '"[^"]*"' ./lib/kern/version.rb | tr -d '"')
|
|
15
14
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
|
16
15
|
|
|
17
16
|
case $VERSION in
|
|
@@ -31,5 +30,5 @@ git push
|
|
|
31
30
|
git tag v$VERSION
|
|
32
31
|
git push --tags
|
|
33
32
|
|
|
34
|
-
rake build
|
|
33
|
+
bundle exec rake build
|
|
35
34
|
gem push "pkg/kern-$VERSION.gem"
|
data/config/routes.rb
CHANGED
|
@@ -2,8 +2,12 @@ Kern::Engine.routes.draw do
|
|
|
2
2
|
resource :settings, only: %w[show]
|
|
3
3
|
namespace :settings do
|
|
4
4
|
resource :user, path: "account", only: %w[show update]
|
|
5
|
+
|
|
6
|
+
resource :subscriptions, only: %w[show]
|
|
5
7
|
end
|
|
6
8
|
|
|
9
|
+
resource :subscriptions, module: :billing_profiles, only: %w[create edit]
|
|
10
|
+
|
|
7
11
|
resource :signup, only: %w[new create]
|
|
8
12
|
|
|
9
13
|
resource :session, only: %w[new create destroy]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateBillingProfiles < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :billing_profiles do |t|
|
|
4
|
+
t.belongs_to :workspace, null: false, foreign_key: true
|
|
5
|
+
t.string :slug, null: true
|
|
6
|
+
t.string :external_id, null: true # This would be payment provider's (Stripe's) customer_id
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :billing_profiles, [:slug, :workspace_id], unique: true
|
|
12
|
+
add_index :billing_profiles, :external_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateBillingProfileSubscriptions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :billing_profile_subscriptions do |t|
|
|
4
|
+
t.belongs_to :billing_profile, null: false, foreign_key: true
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.string :subscription_id, null: false
|
|
7
|
+
t.string :subscription_item_id, null: false
|
|
8
|
+
t.string :status, null: false
|
|
9
|
+
t.datetime :current_period_end_at, null: true
|
|
10
|
+
t.datetime :cancel_at, null: true
|
|
11
|
+
t.json :metadata, null: false, default: {}
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :billing_profile_subscriptions, [:slug, :billing_profile_id], unique: true
|
|
17
|
+
add_index :billing_profile_subscriptions, :subscription_id
|
|
18
|
+
add_index :billing_profile_subscriptions, :subscription_item_id
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -11,6 +11,7 @@ Example:
|
|
|
11
11
|
This will generate sessions and passwords features.
|
|
12
12
|
|
|
13
13
|
Available features:
|
|
14
|
+
billing Billing and subscription functionality
|
|
14
15
|
passwords Password reset functionality
|
|
15
16
|
sessions User authentication (sign in/out)
|
|
16
17
|
settings User settings (user, workspace settings and subscription/billing)
|
|
@@ -2,23 +2,27 @@ module Kern
|
|
|
2
2
|
class FeatureGenerator < Rails::Generators::Base
|
|
3
3
|
source_root File.expand_path("../../../../../", __FILE__)
|
|
4
4
|
|
|
5
|
-
AVAILABLE_FEATURES = %w[passwords sessions settings signups]
|
|
5
|
+
AVAILABLE_FEATURES = %w[billing passwords sessions settings signups]
|
|
6
6
|
|
|
7
7
|
argument :features, type: :array, required: true, banner: "feature feature"
|
|
8
8
|
|
|
9
|
+
class_option :skip_encryption, type: :boolean, default: false, desc: "Skip encryption for sensitive fields"
|
|
10
|
+
|
|
9
11
|
def copy_features
|
|
10
12
|
features.each do |feature|
|
|
11
13
|
verify_exists!(feature)
|
|
12
14
|
|
|
13
15
|
case feature
|
|
14
|
-
when "
|
|
15
|
-
|
|
16
|
-
when "signups"
|
|
17
|
-
generate_signups
|
|
16
|
+
when "billing"
|
|
17
|
+
generate_billing
|
|
18
18
|
when "passwords"
|
|
19
19
|
generate_passwords
|
|
20
|
+
when "sessions"
|
|
21
|
+
generate_sessions
|
|
20
22
|
when "settings"
|
|
21
23
|
generate_settings
|
|
24
|
+
when "signups"
|
|
25
|
+
generate_signups
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -33,39 +37,61 @@ module Kern
|
|
|
33
37
|
exit(1)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
template "app/controllers/concerns/authentication.rb.tt", "app/controllers/concerns/authentication.rb"
|
|
39
|
-
copy_file "app/controllers/kern/sessions_controller.rb", "app/controllers/sessions_controller.rb"
|
|
40
|
-
directory "app/views/kern/sessions", "app/views/sessions"
|
|
40
|
+
def generate_billing
|
|
41
|
+
template "config/initializers/stripe.rb.tt", "config/initializers/stripe.rb"
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
[
|
|
44
|
+
"billing_profile.rb",
|
|
45
|
+
"billing_profile/subscription.rb",
|
|
46
|
+
"workspace/billable.rb"
|
|
47
|
+
].each { copy_file "app/models/#{it}", "app/models/#{it}" }
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
directory "app/models", "app/models" unless features.include?("sessions")
|
|
49
|
+
inject_into_file "app/models/workspace.rb", " include Billable\n\n", after: "include Sluggable\n"
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
route "resource :subscriptions, module: :billing_profiles, only: %w[create edit]"
|
|
52
|
+
inject_into_file "config/routes.rb", " resource :subscriptions, only: %w[show]\n", after: "namespace :settings do\n"
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
copy_file "app/views/kern/settings/subscriptions/show.html.erb", "app/views/settings/subscriptions/show.html.erb"
|
|
55
|
+
copy_file "app/views/kern/settings/subscriptions/_plan.html.erb", "app/views/settings/subscriptions/_plan.html.erb"
|
|
56
|
+
|
|
57
|
+
copy_file "app/controllers/kern/settings/subscriptions_controller.rb", "app/controllers/settings/subscriptions_controller.rb"
|
|
58
|
+
template "app/controllers/kern/billing_profiles/subscriptions_controller.rb.tt", "app/controllers/billing_profiles/subscriptions_controller.rb"
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
def generate_passwords
|
|
55
|
-
directory "app/views/kern/passwords", "app/views/passwords"
|
|
56
62
|
copy_file "app/controllers/kern/passwords_controller.rb", "app/controllers/passwords_controller.rb"
|
|
57
63
|
copy_file "app/mailers/kern/passwords_mailer.rb", "app/mailers/passwords_mailer.rb"
|
|
64
|
+
|
|
58
65
|
template "app/views/kern/passwords_mailer/reset.text.erb.tt", "app/views/passwords_mailer/reset.text.erb"
|
|
59
66
|
template "app/views/kern/passwords_mailer/reset.html.erb.tt", "app/views/passwords_mailer/reset.html.erb"
|
|
60
67
|
|
|
68
|
+
copy_file "app/views/kern/passwords/new.html.erb", "app/views/passwords/new.html.erb"
|
|
69
|
+
copy_file "app/views/kern/passwords/edit.html.erb", "app/views/passwords/edit.html.erb"
|
|
70
|
+
|
|
61
71
|
route "resources :passwords, param: :token, only: %w[new create edit update]"
|
|
62
72
|
end
|
|
63
73
|
|
|
74
|
+
def generate_sessions
|
|
75
|
+
AUTHENTICATION_MODELS.each { copy_file "app/models/#{it}", "app/models/#{it}" }
|
|
76
|
+
|
|
77
|
+
template "app/controllers/concerns/authentication.rb.tt", "app/controllers/concerns/authentication.rb"
|
|
78
|
+
copy_file "app/controllers/kern/sessions_controller.rb", "app/controllers/sessions_controller.rb"
|
|
79
|
+
|
|
80
|
+
copy_file "app/views/kern/sessions/new.html.erb", "app/views/sessions/new.html.erb"
|
|
81
|
+
|
|
82
|
+
remove_encryption_from_models
|
|
83
|
+
|
|
84
|
+
route "resource :session, only: %w[new create destroy]"
|
|
85
|
+
end
|
|
86
|
+
|
|
64
87
|
def generate_settings
|
|
65
|
-
directory "app/views/kern/settings", "app/views/settings"
|
|
66
88
|
copy_file "app/controllers/kern/settings_controller.rb", "app/controllers/settings_controller.rb"
|
|
67
89
|
directory "app/controllers/kern/settings", "app/controllers/settings"
|
|
68
90
|
|
|
91
|
+
copy_file "app/views/kern/settings/show.html.erb", "app/views/settings/show.html.erb"
|
|
92
|
+
copy_file "app/views/kern/settings/_cards.html.erb", "app/views/settings/_cards.html.erb"
|
|
93
|
+
copy_file "app/views/kern/settings/users/show.html.erb", "app/views/settings/users/show.html.erb"
|
|
94
|
+
|
|
69
95
|
route <<~RUBY
|
|
70
96
|
resource :settings, only: %w[show]
|
|
71
97
|
namespace :settings do
|
|
@@ -73,5 +99,42 @@ module Kern
|
|
|
73
99
|
end
|
|
74
100
|
RUBY
|
|
75
101
|
end
|
|
102
|
+
|
|
103
|
+
def generate_signups
|
|
104
|
+
AUTHENTICATION_MODELS.each { copy_file "app/models/#{it}", "app/models/#{it}" } unless features.include?("sessions")
|
|
105
|
+
|
|
106
|
+
template "app/controllers/kern/signups_controller.rb.tt", "app/controllers/signups_controller.rb"
|
|
107
|
+
|
|
108
|
+
copy_file "app/views/kern/signups/new.html.erb", "app/views/signups/new.html.erb"
|
|
109
|
+
|
|
110
|
+
remove_encryption_from_models
|
|
111
|
+
|
|
112
|
+
route "resource :signup, only: %w[new create]"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remove_encryption_from_models
|
|
116
|
+
return unless options[:skip_encryption]
|
|
117
|
+
|
|
118
|
+
gsub_file "app/models/session.rb", /\n encrypts :user_agent, :ip\n/, ""
|
|
119
|
+
gsub_file "app/models/user.rb", /\n encrypts :email, deterministic: true, downcase: true\n/, ""
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
AUTHENTICATION_MODELS = [
|
|
123
|
+
"actor.rb",
|
|
124
|
+
"application_form.rb",
|
|
125
|
+
"current.rb",
|
|
126
|
+
"member.rb",
|
|
127
|
+
"member/acting.rb",
|
|
128
|
+
"member/setup.rb",
|
|
129
|
+
"role.rb",
|
|
130
|
+
"session.rb",
|
|
131
|
+
"signup.rb",
|
|
132
|
+
"user.rb",
|
|
133
|
+
"user/workspace_member.rb",
|
|
134
|
+
"workspace.rb",
|
|
135
|
+
"workspace/feature_access.rb",
|
|
136
|
+
"workspace/members.rb",
|
|
137
|
+
"workspace/setup.rb"
|
|
138
|
+
]
|
|
76
139
|
end
|
|
77
140
|
end
|
|
@@ -8,6 +8,23 @@ module Kern
|
|
|
8
8
|
|
|
9
9
|
class_option :skip_migrations, type: :boolean, default: false
|
|
10
10
|
|
|
11
|
+
def add_gems
|
|
12
|
+
gem "fuik"
|
|
13
|
+
gem "rails_icons"
|
|
14
|
+
gem "rails_vault"
|
|
15
|
+
gem "stripe"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def enable_bcrypt
|
|
19
|
+
if File.read(File.expand_path("Gemfile", destination_root)).include?('gem "bcrypt"')
|
|
20
|
+
uncomment_lines "Gemfile", /gem "bcrypt"/
|
|
21
|
+
|
|
22
|
+
bundle_command("install --quiet")
|
|
23
|
+
else
|
|
24
|
+
bundle_command("add bcrypt", {}, quiet: true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
11
28
|
def copy_migrations
|
|
12
29
|
return if options[:skip_migrations]
|
|
13
30
|
|
|
@@ -26,23 +43,42 @@ module Kern
|
|
|
26
43
|
end
|
|
27
44
|
end
|
|
28
45
|
|
|
29
|
-
def
|
|
46
|
+
def copy_configurations
|
|
30
47
|
template "configurations/urls.yml", "config/configurations/urls.yml"
|
|
48
|
+
template "configurations/stripe.yml", "config/configurations/stripe.yml"
|
|
49
|
+
template "configurations/plans.yml", "config/configurations/plans.yml"
|
|
50
|
+
|
|
31
51
|
template "configurations/README.md", "config/configurations/README.md"
|
|
32
52
|
end
|
|
33
53
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
def setup_workspace_access
|
|
55
|
+
Bundler.with_unbundled_env do
|
|
56
|
+
rails_command "generate rails_vault:install"
|
|
57
|
+
end
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
source_paths.unshift(File.expand_path("../../../..", __dir__))
|
|
60
|
+
template "app/models/workspace/access.rb.tt", "app/models/workspace/access.rb"
|
|
61
|
+
source_paths.shift
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def install_fuik
|
|
65
|
+
Bundler.with_unbundled_env do
|
|
66
|
+
rails_command "generate fuik:install"
|
|
41
67
|
end
|
|
68
|
+
|
|
69
|
+
route "mount Fuik::Engine, at: '/'" if Rails.env.test? # ¯\_(ツ)_/¯
|
|
70
|
+
|
|
71
|
+
source_paths.unshift(File.expand_path("../../../..", __dir__))
|
|
72
|
+
directory "app/webhooks/stripe", "app/webhooks/stripe"
|
|
73
|
+
source_paths.shift
|
|
42
74
|
end
|
|
43
75
|
|
|
44
76
|
def mount_engine
|
|
45
77
|
route 'mount Kern::Engine => "/"'
|
|
46
78
|
end
|
|
79
|
+
|
|
80
|
+
def post_install_message
|
|
81
|
+
readme "POST_INSTALL" if behavior == :invoke
|
|
82
|
+
end
|
|
47
83
|
end
|
|
48
84
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
Kern is ready. One quick setup step.
|
|
3
|
+
|
|
4
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5
|
+
|
|
6
|
+
REQUIRED SETUP
|
|
7
|
+
|
|
8
|
+
Encryption keys
|
|
9
|
+
|
|
10
|
+
Kern uses Active Record Encryption to encrypt sensitive user data
|
|
11
|
+
(email address, session IP and user agent). Run:
|
|
12
|
+
|
|
13
|
+
$ bin/rails db:encryption:init
|
|
14
|
+
|
|
15
|
+
This will output the required encryption keys you can add to your credential
|
|
16
|
+
files (bin/rails credentials:edit --environment=development).
|
|
17
|
+
|
|
18
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
19
|
+
|
|
20
|
+
Next steps:
|
|
21
|
+
|
|
22
|
+
1. Run migrations: bin/rails db:migrate
|
|
23
|
+
2. Start your server: bin/dev
|
|
24
|
+
3. Check out the full documentation at https://saas.railsdesigner.com/
|
|
25
|
+
|