mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-25 08:09:38 +02:00
Billing (#1269)
* Change env SELF_HOSTING_ENABLED to SELF_HOSTED * Initial Stripe implementation * Fix portal link * Use webhook signatures * Migrated to new Stripe gem conventions Also updated resource routing * Added faraday-multipart gem to resolve middleware notice * Merge fix * Merge fix * Temporary upgrade prompt for early access * Lint fix * i18n fixes * Remove catch-all rescue * Update .env.example
This commit is contained in:
parent
41dff228e8
commit
31f3ff6a16
27 changed files with 225 additions and 12 deletions
2
app/controllers/settings/billings_controller.rb
Normal file
2
app/controllers/settings/billings_controller.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class Settings::BillingsController < SettingsController
|
||||
end
|
36
app/controllers/subscriptions_controller.rb
Normal file
36
app/controllers/subscriptions_controller.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
class SubscriptionsController < ApplicationController
|
||||
def new
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
if Current.family.stripe_customer_id.blank?
|
||||
customer = client.v1.customers.create(
|
||||
email: Current.family.primary_user.email,
|
||||
metadata: { family_id: Current.family.id }
|
||||
)
|
||||
Current.family.update(stripe_customer_id: customer.id)
|
||||
end
|
||||
|
||||
session = client.v1.checkout.sessions.create({
|
||||
customer: Current.family.stripe_customer_id,
|
||||
line_items: [ {
|
||||
price: ENV["STRIPE_PLAN_ID"],
|
||||
quantity: 1
|
||||
} ],
|
||||
mode: "subscription",
|
||||
success_url: settings_billing_url,
|
||||
cancel_url: settings_billing_url
|
||||
})
|
||||
|
||||
redirect_to session.url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
|
||||
def show
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
portal_session = client.v1.billing_portal.sessions.create(
|
||||
customer: Current.family.stripe_customer_id,
|
||||
return_url: settings_billing_url
|
||||
)
|
||||
redirect_to portal_session.url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
end
|
61
app/controllers/webhooks_controller.rb
Normal file
61
app/controllers/webhooks_controller.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
class WebhooksController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token, only: [ :stripe ]
|
||||
skip_authentication
|
||||
|
||||
def stripe
|
||||
webhook_body = request.body.read
|
||||
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
|
||||
begin
|
||||
thin_event = client.parse_thin_event(webhook_body, sig_header, ENV["STRIPE_WEBHOOK_SECRET"])
|
||||
|
||||
event = client.v1.events.retrieve(thin_event.id)
|
||||
|
||||
case event.type
|
||||
when /^customer\.subscription\./
|
||||
handle_subscription_event(event)
|
||||
when "customer.created", "customer.updated", "customer.deleted"
|
||||
handle_customer_event(event)
|
||||
else
|
||||
Rails.logger.info "Unhandled event type: #{event.type}"
|
||||
end
|
||||
|
||||
rescue JSON::ParserError
|
||||
render json: { error: "Invalid payload" }, status: :bad_request
|
||||
return
|
||||
rescue Stripe::SignatureVerificationError
|
||||
render json: { error: "Invalid signature" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
render json: { received: true }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_subscription_event(event)
|
||||
subscription = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: subscription.customer)
|
||||
|
||||
if family
|
||||
family.update(
|
||||
stripe_plan_id: subscription.plan.id,
|
||||
stripe_subscription_status: subscription.status
|
||||
)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{subscription.customer}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_customer_event(event)
|
||||
customer = event.data.object
|
||||
family = Family.find_by(stripe_customer_id: customer.id)
|
||||
|
||||
if family
|
||||
family.update(stripe_customer_id: customer.id)
|
||||
else
|
||||
Rails.logger.error "Family not found for Stripe customer ID: #{customer.id}"
|
||||
end
|
||||
end
|
||||
end
|
2
app/helpers/settings/billing_helper.rb
Normal file
2
app/helpers/settings/billing_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module Settings::BillingHelper
|
||||
end
|
|
@ -3,6 +3,7 @@ module SettingsHelper
|
|||
{ name: I18n.t("settings.nav.profile_label"), path: :settings_profile_path },
|
||||
{ name: I18n.t("settings.nav.preferences_label"), path: :settings_preferences_path },
|
||||
{ name: I18n.t("settings.nav.self_hosting_label"), path: :settings_hosting_path, condition: :self_hosted? },
|
||||
{ name: I18n.t("settings.nav.billing_label"), path: :settings_billing_path },
|
||||
{ name: I18n.t("settings.nav.accounts_label"), path: :accounts_path },
|
||||
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
|
||||
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
|
||||
|
|
2
app/helpers/subscription_helper.rb
Normal file
2
app/helpers/subscription_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module SubscriptionHelper
|
||||
end
|
2
app/helpers/webhooks_helper.rb
Normal file
2
app/helpers/webhooks_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module WebhooksHelper
|
||||
end
|
|
@ -127,4 +127,12 @@ class Family < ApplicationRecord
|
|||
def synth_usage
|
||||
self.class.synth_provider&.usage
|
||||
end
|
||||
|
||||
def subscribed?
|
||||
stripe_subscription_status.present? && stripe_subscription_status == "active"
|
||||
end
|
||||
|
||||
def primary_user
|
||||
users.order(:created_at).first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<% if @accounts.empty? %>
|
||||
<% if !Current.family.subscribed? && !self_hosted? %>
|
||||
<%= render "shared/subscribe_prompt" %>
|
||||
<% elsif @accounts.empty? %>
|
||||
<%= render "shared/no_account_empty_state" %>
|
||||
<% else %>
|
||||
<section class="flex gap-4">
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<%= sidebar_link_to t(".self_hosting_label"), settings_hosting_path, icon: "database" %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".billing_label"), settings_billing_path, icon: "circle-dollar-sign" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= sidebar_link_to t(".accounts_label"), accounts_path, icon: "layers" %>
|
||||
</li>
|
||||
|
|
16
app/views/settings/billings/show.html.erb
Normal file
16
app/views/settings/billings/show.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<% content_for :sidebar do %>
|
||||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
|
||||
<% if Current.family.stripe_plan_id.blank? %>
|
||||
<%= link_to t(".subscribe_button"), new_subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
<% else %>
|
||||
<%= link_to t(".manage_subscription_button"), subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_nav_footer %>
|
||||
</div>
|
16
app/views/shared/_subscribe_prompt.html.erb
Normal file
16
app/views/shared/_subscribe_prompt.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="flex justify-center items-center h-[800px]">
|
||||
<div class="text-center flex flex-col gap-4 items-center max-w-[300px]">
|
||||
<%= lucide_icon "circle-fading-arrow-up", class: "w-8 h-8 text-green-500" %>
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
<p class="text-gray-900 font-medium"><%= t(".title") %></p>
|
||||
<p class="text-gray-500"><%= t(".subtitle") %></p>
|
||||
<p class="text-gray-400 text-xs"><%= t(".guarantee") %></p>
|
||||
</div>
|
||||
|
||||
<%= link_to new_subscription_path, class: "btn btn--primary flex items-center gap-1" do %>
|
||||
<%= lucide_icon("credit-card", class: "w-5 h-5") %>
|
||||
<span><%= t(".subscribe") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue