Add billing system with Stripe
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Jason Zimdars <jz@37signals.com>
This commit is contained in:
+1
-1
@@ -3,8 +3,8 @@ eval_gemfile "Gemfile"
|
||||
|
||||
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
||||
|
||||
# SaaS-only functionality
|
||||
gem "activeresource", require: "active_resource"
|
||||
gem "stripe", "~> 18.0"
|
||||
gem "queenbee", bc: "queenbee-plugin"
|
||||
gem "fizzy-saas", bc: "fizzy-saas"
|
||||
gem "console1984", bc: "console1984"
|
||||
|
||||
+3
-1
@@ -21,7 +21,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/fizzy-saas
|
||||
revision: f0bc5d811ff80b45dc44b88a13a0a17a3e823143
|
||||
revision: 8eaa692316bb27ed73bd628b4735082585498ca0
|
||||
specs:
|
||||
fizzy-saas (0.1.0)
|
||||
audits1984
|
||||
@@ -558,6 +558,7 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.9)
|
||||
stripe (18.0.1)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.17)
|
||||
thruster (0.1.17-aarch64-linux)
|
||||
@@ -684,6 +685,7 @@ DEPENDENCIES
|
||||
solid_queue (~> 1.2)
|
||||
sqlite3 (>= 2.0)
|
||||
stimulus-rails
|
||||
stripe (~> 18.0)
|
||||
thruster
|
||||
trilogy (~> 2.9)
|
||||
turbo-rails
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
33% { background-color: var(--color-border-darker); scale: 1; }
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0% { transform: rotate(0deg); }
|
||||
20% { transform: rotate(3deg); }
|
||||
40% { transform: rotate(-3deg); }
|
||||
60% { transform: rotate(3deg); }
|
||||
80% { transform: rotate(-3deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes wobble {
|
||||
0% { transform: rotate(calc(var(--bubble-rotate) + 30deg)); }
|
||||
15% { border-radius: 66% 34% 72% 28% / 39% 63% 37% 61%; }
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
border-radius: 99rem;
|
||||
}
|
||||
|
||||
.btn:not(.popup__btn, .btn--plain, .btn--reversed) {
|
||||
.btn:not(.popup__btn, .btn--plain, .btn--reversed, .settings-subscription__button) {
|
||||
--btn-background: var(--card-color);
|
||||
--btn-color: var(--color-ink-inverted);
|
||||
}
|
||||
|
||||
@@ -184,6 +184,23 @@
|
||||
.fill-highlight { background-color: var(--color-highlight); }
|
||||
.fill-transparent { background-color: transparent; }
|
||||
|
||||
.fill-highlighter {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-highlight);
|
||||
border-radius: 0.2em;
|
||||
content: "";
|
||||
inset-block: 0;
|
||||
inset-inline: -0.1em;
|
||||
position: absolute;
|
||||
transform: skewX(-10deg) rotate(1deg);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.translucent { opacity: var(--opacity, 0.66); }
|
||||
|
||||
/* Borders */
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
class Admin::StatsController < AdminController
|
||||
layout "public"
|
||||
|
||||
def show
|
||||
@accounts_total = Account.count
|
||||
@accounts_last_7_days = Account.where(created_at: 7.days.ago..).count
|
||||
@accounts_last_24_hours = Account.where(created_at: 24.hours.ago..).count
|
||||
|
||||
@identities_total = Identity.count
|
||||
@identities_last_7_days = Identity.where(created_at: 7.days.ago..).count
|
||||
@identities_last_24_hours = Identity.where(created_at: 24.hours.ago..).count
|
||||
|
||||
@top_accounts = Account
|
||||
.where("cards_count > 0")
|
||||
.order(cards_count: :desc)
|
||||
.limit(20)
|
||||
|
||||
@recent_accounts = Account.order(created_at: :desc).limit(10)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
module Admin
|
||||
end
|
||||
@@ -20,3 +20,5 @@
|
||||
<%= render "account/settings/export" %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%= render "account/settings/subscription_panel" if Fizzy.saas? %>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<% @page_title = "Account Statistics" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<h1 class="header__title"><%= @page_title %></h1>
|
||||
<% end %>
|
||||
|
||||
<section class="settings">
|
||||
<div class="settings__panel panel shadow center">
|
||||
<div class="flex flex-column gap margin-block-half">
|
||||
<div class="flex flex-column gap-half">
|
||||
<header>
|
||||
<h2 class="divider txt-medium margin-block-start">Accounts Created</h2>
|
||||
</header>
|
||||
<div class="flex gap-half">
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">Total</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @accounts_total %></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">7 days</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @accounts_last_7_days %></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">24 hours</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @accounts_last_24_hours %></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-half">
|
||||
<header>
|
||||
<h2 class="divider txt-medium margin-block-start">Identities Created</h2>
|
||||
</header>
|
||||
<div class="flex gap-half">
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">Total</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @identities_total %></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">7 days</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @identities_last_7_days %></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
||||
<div class="txt-x-small">24 hours</div>
|
||||
<div class="txt-large">
|
||||
<strong><%= @identities_last_24_hours %></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings__panel panel shadow center">
|
||||
<header>
|
||||
<h2 class="divider txt-medium margin-block-start">
|
||||
10 Most Recent Signups
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<ul class="margin-block-half">
|
||||
<% @recent_accounts.each do |account| %>
|
||||
<% admin_user = account.users.owner.first %>
|
||||
<li class="flex align-start gap-half margin-block-start-half">
|
||||
<div
|
||||
class="flex-item-grow min-width overflow-ellipsis"
|
||||
style="text-align: left"
|
||||
>
|
||||
<strong><%= account.name %></strong>
|
||||
<br>
|
||||
<span class="txt-x-small txt-ink-medium">
|
||||
#<%= account.external_account_id %> •
|
||||
<%= admin_user&.identity&.email_address || "No admin" %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="txt-medium txt-ink-medium" style="text-align: right">
|
||||
<%= time_ago_in_words(account.created_at) %> ago
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="settings__panel panel shadow center">
|
||||
<header>
|
||||
<h2 class="divider txt-medium margin-block-start">
|
||||
Top 20 Accounts by Card Count
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<ul class="margin-block-half">
|
||||
<% @top_accounts.each do |account| %>
|
||||
<% admin_user = account.users.owner.first %>
|
||||
<li class="flex align-start gap-half margin-block-start-half">
|
||||
<div
|
||||
class="flex-item-grow min-width overflow-ellipsis"
|
||||
style="text-align: left"
|
||||
>
|
||||
<strong><%= account.name %></strong>
|
||||
<br>
|
||||
<span class="txt-x-small txt-ink-medium">
|
||||
#<%= account.external_account_id %> •
|
||||
<%= admin_user&.identity&.email_address || "No admin" %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="txt-medium">
|
||||
<strong><%= number_with_delimiter(account.cards_count) %></strong>
|
||||
<span class="txt-x-small txt-ink-medium">cards</span>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,5 +1,5 @@
|
||||
<% cache card do %>
|
||||
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
|
||||
<section id="<%= dom_id(card, :card_container) %>" class="card-perma" style="--card-color: <%= card.color %>;">
|
||||
<% cache card do %>
|
||||
<div class="card-perma__actions card-perma__actions--left">
|
||||
<%= render "cards/container/gild", card: card if card.published? && !card.closed? %>
|
||||
<%= render "cards/container/image", card: card %>
|
||||
@@ -33,11 +33,15 @@
|
||||
<%= render "cards/display/preview/bubble", card: card %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if card.published? %>
|
||||
<%= render "cards/container/footer/published", card: card %>
|
||||
<% elsif card.drafted? %>
|
||||
<%= render "cards/container/footer/draft", card: card %>
|
||||
<% if card.published? %>
|
||||
<%= render "cards/container/footer/published", card: card %>
|
||||
<% elsif card.drafted? %>
|
||||
<% if Fizzy.saas? %>
|
||||
<%= render "cards/container/footer/saas/create", card: card %>
|
||||
<% else %>
|
||||
<%= render "cards/container/footer/create", card: card %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="card-perma__notch card-perma__notch--bottom flex-column">
|
||||
<div class="flex gap-half">
|
||||
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
|
||||
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
|
||||
<span>Create card</span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
|
||||
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
|
||||
<span>Create and add another</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "cards/container/footer/saas/near_notice" if Fizzy.saas? %>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="card-perma__notch card-perma__notch--bottom">
|
||||
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
|
||||
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
|
||||
form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
|
||||
<span>Create card</span>
|
||||
<% end %>
|
||||
|
||||
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
|
||||
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
|
||||
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
|
||||
<span>Create and add another</span>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<%= render "layouts/theme_preference" %>
|
||||
<%= stylesheet_link_tags %>
|
||||
<%= stylesheet_link_tag "fizzy/saas", "data-turbo-track": "reload" if Fizzy.saas? %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<%= tenanted_action_cable_meta_tag %>
|
||||
|
||||
+1
-2
@@ -2,9 +2,9 @@ Rails.application.routes.draw do
|
||||
root "events#index"
|
||||
|
||||
namespace :account do
|
||||
resource :entropy
|
||||
resource :join_code
|
||||
resource :settings
|
||||
resource :entropy
|
||||
resources :exports, only: [ :create, :show ]
|
||||
end
|
||||
|
||||
@@ -239,6 +239,5 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :admin do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
get "stats", to: "stats#show"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Admin::StatsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "staff can access stats" do
|
||||
sign_in_as :david
|
||||
|
||||
untenanted do
|
||||
get admin_stats_url
|
||||
end
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "non-staff cannot access stats" do
|
||||
sign_in_as :jz
|
||||
|
||||
untenanted do
|
||||
get admin_stats_url
|
||||
end
|
||||
|
||||
assert_response :forbidden
|
||||
end
|
||||
end
|
||||
Vendored
+6
@@ -21,3 +21,9 @@ private_kevin:
|
||||
account: 37s_uuid
|
||||
board: private_uuid
|
||||
user: kevin_uuid
|
||||
|
||||
miltons_wish_list_mike:
|
||||
id: <%= ActiveRecord::FixtureSet.identify("miltons_wish_list_mike", :uuid) %>
|
||||
account: initech_uuid
|
||||
board: miltons_wish_list_uuid
|
||||
user: mike_uuid
|
||||
|
||||
Vendored
+11
@@ -79,3 +79,14 @@ paycheck:
|
||||
status: published
|
||||
last_active_at: <%= 1.week.ago %>
|
||||
account: initech_uuid
|
||||
|
||||
unfinished_thoughts:
|
||||
id: <%= ActiveRecord::FixtureSet.identify("unfinished_thoughts", :uuid) %>
|
||||
number: 3
|
||||
board: miltons_wish_list_uuid
|
||||
creator: mike_uuid
|
||||
title: Some unfinished thoughts
|
||||
created_at: <%= 1.week.ago %>
|
||||
status: drafted
|
||||
last_active_at: <%= 1.week.ago %>
|
||||
account: initech_uuid
|
||||
|
||||
@@ -81,7 +81,7 @@ class CardTest < ActiveSupport::TestCase
|
||||
|
||||
test "open" do
|
||||
assert_equal cards(:logo, :layout, :text, :buy_domain).to_set, accounts("37s").cards.open.to_set
|
||||
assert_equal cards(:radio, :paycheck).to_set, accounts("initech").cards.open.to_set
|
||||
assert_equal cards(:radio, :paycheck, :unfinished_thoughts).to_set, accounts("initech").cards.open.to_set
|
||||
end
|
||||
|
||||
test "card_unassigned" do
|
||||
|
||||
Reference in New Issue
Block a user