This repository was archived by the owner on Dec 16, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Add monthly subscriptions #23
Merged
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
069a72a
Add Stripe-based billing system
jorgemanrubia 3804161
Refresh the subscription instead of relying on event payloads
jorgemanrubia 4641d88
Let's use a single sandbox environment via 1password ENV vars
jorgemanrubia a7769db
Add script to set stripe env vars from 1password
jorgemanrubia 6c164fe
Dont't fail if not set
jorgemanrubia 94c7ed6
Kill previous tunnel automatically
jorgemanrubia 6e45e61
Document how to work with Stripe locally
jorgemanrubia 78f5a02
Add stripe envs
jorgemanrubia 845f9db
Fix typo with unlimitted
jorgemanrubia 70da6b2
Fix typo
jorgemanrubia 7df2906
Paid plans never exceed limits!
jorgemanrubia 56bedaf
Spelling
jzimdars 09cf2ca
Fancy apostrophes
jzimdars 64f6866
Typo
jzimdars d4fe376
Layout
jzimdars 7fd0d33
More fancy apostrophes
jzimdars 9721f29
Show different things to non-admins
jzimdars 97e09ea
Give this some padding
jzimdars c253d44
Don't prevent creating drafts, or you won't get to see the upgrade ba…
jorgemanrubia 0f3d1da
Remove redundant condition
jorgemanrubia e30538c
Enable automatic taxes and calculate based on mandatory billing address
jorgemanrubia 19cbcf4
Add record to track overridden limits
jorgemanrubia 8106f43
Add system to comp accounts
jorgemanrubia 24fd5b9
Grab and show the next amount to pay from Stripe
jorgemanrubia 1062777
Enable tax id collection
jorgemanrubia 4f7650b
This is needed for vat id collection
jorgemanrubia 3f96baf
Rename param to remove redundant bit
jorgemanrubia 54d5ff8
Prefer if/else over early return
jorgemanrubia 640a09b
More concise
jorgemanrubia febd9ca
Make active_subscription a private method
jorgemanrubia 6247c90
Extract method
jorgemanrubia b20ca22
Rename to stripe_session to avoid confusions
jorgemanrubia 6b7fea7
Format
jorgemanrubia bbfb8f5
Move presentation method to a regular view helper
jorgemanrubia 047b4bc
Remove nested if
jorgemanrubia 3a2d2ad
Format
jorgemanrubia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /* Subscriptions | ||
| /* ------------------------------------------------------------------------ */ | ||
| :root { | ||
| --settings-subscription-background: linear-gradient(to bottom, var(--color-canvas), oklch(var(--lch-violet-lighter))); | ||
| --settings-subscription-color: oklch(var(--lch-violet-medium)); | ||
| --settings-subscription-text-color: oklch(var(--lch-violet-dark)); | ||
|
|
||
| .settings-subscription__button { | ||
| --btn-background: var(--settings-subscription-color); | ||
| --btn-border-color: var(--color-canvas); | ||
| --btn-color: var(--color-canvas); | ||
| --focus-ring-color: var(--color-ink); | ||
| } | ||
|
|
||
| .settings-subscription__divider { | ||
| --divider-color: currentColor; | ||
|
|
||
| color: var(--settings-subscription-color); | ||
| margin-block-start: calc(var(--block-space-half) * -1); | ||
| } | ||
|
|
||
| .settings-subscription__footer { | ||
| color: var(--settings-subscription-text-color); | ||
|
|
||
| &::before { | ||
| content: "————"; | ||
| display: block; | ||
| margin: auto; | ||
| text-align: center; | ||
| } | ||
| } | ||
|
|
||
| .settings-subscription__notch { | ||
| animation: wiggle 500ms ease; | ||
| background: var(--settings-subscription-background); | ||
| box-shadow: 0 0 0.3em 0.2em var(--settings-subscription-color); | ||
| border-radius: 3em; | ||
| color: var(--settings-subscription-text-color); | ||
| margin-inline: auto; | ||
| padding: 0 0.3em 0 1.2em; | ||
| transform: rotate(-1deg); | ||
|
|
||
| @media (max-width: 640px) { | ||
| padding-block: 0.6em; | ||
| padding-inline-start: 0.3em; | ||
| } | ||
|
|
||
| .btn { | ||
| margin-block: 0.3em; | ||
| } | ||
| } | ||
|
|
||
| .settings-subscription__panel { | ||
| background: var(--settings-subscription-background); | ||
| } | ||
|
|
||
| .settings_subscription__warning { | ||
| color: var(--settings-subscription-text-color); | ||
|
|
||
| a { | ||
| color: var(--settings-subscription-text-color); | ||
| } | ||
| } | ||
| } |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| class Account::BillingPortalsController < ApplicationController | ||
| before_action :ensure_admin | ||
| before_action :ensure_subscribed_account | ||
|
|
||
| def show | ||
| redirect_to create_stripe_billing_portal_session.url, allow_other_host: true | ||
| end | ||
|
|
||
| private | ||
| def ensure_subscribed_account | ||
| unless Current.account.subscribed? | ||
| redirect_to account_subscription_path, alert: "No billing information found" | ||
| end | ||
| end | ||
|
|
||
| def create_stripe_billing_portal_session | ||
| Stripe::BillingPortal::Session.create(customer: Current.account.subscription.stripe_customer_id, return_url: account_settings_url) | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| class Account::SubscriptionsController < ApplicationController | ||
| before_action :ensure_admin | ||
| before_action :set_stripe_session, only: :show | ||
|
|
||
| def show | ||
| end | ||
|
|
||
| def create | ||
| session = Stripe::Checkout::Session.create \ | ||
| customer: find_or_create_stripe_customer, | ||
| mode: "subscription", | ||
| line_items: [ { price: Plan.paid.stripe_price_id, quantity: 1 } ], | ||
| success_url: account_subscription_url + "?session_id={CHECKOUT_SESSION_ID}", | ||
| cancel_url: account_subscription_url, | ||
| metadata: { account_id: Current.account.id, plan_key: Plan.paid.key }, | ||
| automatic_tax: { enabled: true }, | ||
| tax_id_collection: { enabled: true }, | ||
| billing_address_collection: "required", | ||
| customer_update: { address: "auto", name: "auto" } | ||
|
|
||
| redirect_to session.url, allow_other_host: true | ||
| end | ||
|
|
||
| private | ||
| def set_stripe_session | ||
| @stripe_session = Stripe::Checkout::Session.retrieve(params[:session_id]) if params[:session_id] | ||
| end | ||
|
|
||
| def find_or_create_stripe_customer | ||
| find_stripe_customer || create_stripe_customer | ||
| end | ||
|
|
||
| def find_stripe_customer | ||
| Stripe::Customer.retrieve(Current.account.subscription.stripe_customer_id) if Current.account.subscription&.stripe_customer_id | ||
| end | ||
|
|
||
| def create_stripe_customer | ||
| Stripe::Customer.create(email: Current.user.identity.email_address, name: Current.account.name, metadata: { account_id: Current.account.id }).tap do |customer| | ||
| Current.account.create_subscription!(stripe_customer_id: customer.id, plan_key: Plan.paid.key, status: "incomplete") | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| class Admin::AccountSearchesController < AdminController | ||
| def create | ||
| redirect_to saas.edit_admin_account_path(params[:q]) | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| class Admin::AccountsController < AdminController | ||
| include Admin::AccountScoped | ||
|
|
||
| layout "public" | ||
|
|
||
| before_action :set_account, only: %i[ edit update ] | ||
|
|
||
| def index | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a simple admin screen where you can modify the card count in a given account. It is intended mostly to test this in staging. |
||
| end | ||
|
|
||
| def edit | ||
| end | ||
|
|
||
| def update | ||
| @account.override_limits(card_count: overridden_card_count_param) | ||
| redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account limits updated" | ||
| end | ||
|
|
||
| private | ||
| def set_account | ||
| @account = Account.find_by!(external_account_id: params[:id]) | ||
| end | ||
|
|
||
| def overridden_card_count_param | ||
| params[:account][:overridden_card_count].to_i | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| class Admin::BillingWaiversController < AdminController | ||
| include Admin::AccountScoped | ||
|
|
||
| def create | ||
| @account.comp | ||
| redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account comped" | ||
| end | ||
|
|
||
| def destroy | ||
| @account.uncomp | ||
| redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account uncomped" | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| class Admin::OverriddenLimitsController < AdminController | ||
| include Admin::AccountScoped | ||
|
|
||
| def destroy | ||
| @account.reset_overridden_limits | ||
| redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Limits reset" | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| class Admin::StatsController < AdminController | ||
| layout "public" | ||
|
|
||
| def show | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to the gem, but unrelated to the billing system. |
||
| @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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| module Admin::AccountScoped | ||
| extend ActiveSupport::Concern | ||
|
|
||
| included do | ||
| before_action :set_account | ||
| end | ||
|
|
||
| private | ||
| def set_account | ||
| @account = Account.find_by!(external_account_id: params[:account_id] || params[:id]) | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| module Card::LimitedCreation | ||
| extend ActiveSupport::Concern | ||
|
|
||
| included do | ||
| # Only limit API requests. We let you create drafts in the app to actually show the banner, no matter the card count. | ||
| # We limit card publications separately. See +Card::LimitedPublishing+. | ||
| before_action :ensure_can_create_cards, only: %i[ create ], if: -> { request.format.json? } | ||
| end | ||
|
|
||
| private | ||
| def ensure_can_create_cards | ||
| head :forbidden if Current.account.exceeding_card_limit? | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| module Card::LimitedPublishing | ||
| extend ActiveSupport::Concern | ||
|
|
||
| included do | ||
| before_action :ensure_can_publish_cards, only: %i[ create ] | ||
| end | ||
|
|
||
| private | ||
| def ensure_can_publish_cards | ||
| head :forbidden if Current.account.exceeding_card_limit? | ||
| end | ||
| end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.