1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 07:25:19 +02:00

Fix subscription endpoints

This commit is contained in:
Zach Gollwitzer 2025-05-01 16:35:02 -04:00
parent 1638c1e68a
commit 780dc76dce
11 changed files with 68 additions and 46 deletions

View file

@ -2,26 +2,10 @@ class ApplicationController < ActionController::Base
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable, Breadcrumbable, FeatureGuardable, Notifiable
include Pagy::Backend
helper_method :require_upgrade?, :subscription_pending?
before_action :detect_os
before_action :set_default_chat
private
def require_upgrade?
return false if self_hosted?
return false unless Current.session
return false if Current.family.subscribed?
return false if subscription_pending? || request.path == settings_billing_path
true
end
def subscription_pending?
subscribed_at = Current.session.subscribed_at
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
end
def detect_os
user_agent = request.user_agent
@os = case user_agent

View file

@ -3,16 +3,24 @@ module Onboardable
included do
before_action :require_onboarding_and_upgrade
helper_method :subscription_pending?
end
private
# A subscription goes into "pending" mode immediately after checkout, but before webhooks are processed.
def subscription_pending?
subscribed_at = Current.session.subscribed_at
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
end
# First, we require onboarding, then once that's complete, we require an upgrade for non-subscribed users.
def require_onboarding_and_upgrade
return unless Current.user
return unless redirectable_path?(request.path)
if Current.user.onboarded_at.blank?
redirect_to onboarding_path
elsif !Current.family.subscribed?
elsif !Current.family.subscribed? && !Current.family.trialing?
redirect_to upgrade_subscription_path
end
end

View file

@ -38,7 +38,7 @@ class SubscriptionsController < ApplicationController
end
checkout_session_url = stripe.get_checkout_session_url(
price_id,
price_id: price_id,
customer_id: Current.family.stripe_customer_id,
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: upgrade_subscription_url(plan: params[:plan])
@ -49,7 +49,7 @@ class SubscriptionsController < ApplicationController
def show
portal_session_url = stripe.get_billing_portal_session_url(
Current.family.stripe_customer_id,
customer_id: Current.family.stripe_customer_id,
return_url: settings_billing_url
)

View file

@ -2,7 +2,7 @@ class Demo::Generator
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
# Builds a semi-realistic mirror of what production data might look like
def reset_and_clear_data!(family_names)
def reset_and_clear_data!(family_names, require_onboarding: false)
puts "Clearing existing data..."
destroy_everything!
@ -10,7 +10,7 @@ class Demo::Generator
puts "Data cleared"
family_names.each_with_index do |family_name, index|
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", require_onboarding: require_onboarding)
end
puts "Users reset"
@ -152,7 +152,7 @@ class Demo::Generator
Security::Price.destroy_all
end
def create_family_and_user!(family_name, user_email, currency: "USD", subscription_status: "active")
def create_family_and_user!(family_name, user_email, currency: "USD", require_onboarding: false)
base_uuid = "d99e3c6e-d513-4452-8f24-dc263f8528c0"
id = Digest::UUID.uuid_v5(base_uuid, family_name)
@ -160,7 +160,7 @@ class Demo::Generator
id: id,
name: family_name,
currency: currency,
stripe_subscription_status: subscription_status,
stripe_subscription_status: require_onboarding ? nil : "active",
locale: "en",
country: "US",
timezone: "America/New_York",
@ -173,7 +173,7 @@ class Demo::Generator
last_name: "User",
role: "admin",
password: "password",
onboarded_at: Time.current
onboarded_at: require_onboarding ? nil : Time.current
family.users.create! \
email: "member_#{user_email}",
@ -181,7 +181,7 @@ class Demo::Generator
last_name: "User",
role: "member",
password: "password",
onboarded_at: Time.current
onboarded_at: require_onboarding ? nil : Time.current
end
def create_rules!(family)

View file

@ -128,11 +128,15 @@ class Family < ApplicationRecord
end
def subscribed?
trialing? || stripe_subscription_status == "active"
stripe_subscription_status == "active"
end
def trialing?
trial_started_at.present? && trial_started_at <= 14.days.from_now
!subscribed? && trial_started_at.present? && trial_started_at <= 14.days.from_now
end
def trial_remaining_days
(14 - (Time.current - trial_started_at).to_i / 86400).to_i
end
def existing_customer?

View file

@ -30,14 +30,14 @@ class Provider::Stripe
end
end
def create_customer(email, metadata: {})
def create_customer(email:, metadata: {})
client.v1.customers.create(
email: email,
metadata: metadata
)
end
def get_checkout_session_url(price_id, customer_id: nil, success_url: nil, cancel_url: nil)
def get_checkout_session_url(price_id:, customer_id: nil, success_url: nil, cancel_url: nil)
client.v1.checkout.sessions.create(
customer: customer_id,
line_items: [ { price: price_id, quantity: 1 } ],
@ -48,7 +48,7 @@ class Provider::Stripe
).url
end
def get_billing_portal_session_url(customer_id, return_url: nil)
def get_billing_portal_session_url(customer_id:, return_url: nil)
client.v1.billing_portal.sessions.create(
customer: customer_id,
return_url: return_url

View file

@ -24,23 +24,25 @@
</div>
<div class="flex flex-col md:flex-row md:justify-between md:items-center md:gap-4 space-y-4 md:space-y-0 mb-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :first_name, placeholder: "First name", label: "First name", container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :last_name, placeholder: "Last name", label: "Last name", container_class: "bg-container md:w-1/2 w-full", required: true %>
</div>
<% unless @invitation %>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.text_field :name, placeholder: t(".household_name"), label: t(".household_name") %>
<%= family_form.text_field :name, placeholder: "Household name", label: "Household name" %>
<%= family_form.select :country,
country_options,
{ label: t(".country") }, required: true %>
{ label: "Country" },
required: true
%>
<% end %>
</div>
<% end %>
<%= form.submit t(".submit") %>
<%= form.submit "Continue" %>
<% end %>
</div>
</div>

View file

@ -4,12 +4,25 @@
<div class="space-y-4">
<div class="p-3 shadow-border-xs bg-container rounded-lg flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= icon "gem" %>
</div>
<%= render FilledIconComponent.new(
icon: "gem",
rounded: true,
size: "lg"
) %>
<div class="text-sm space-y-1">
<% if @user.family.subscribed? || subscription_pending? %>
<% if subscription_pending? %>
<p class="text-primary">
Your subscription is pending. You can still use Maybe+ while we process your subscription.
</p>
<% elsif @user.family.trialing? %>
<p class="text-primary">
You are currently trialing <span class="font-medium">Maybe+</span>
<span class="text-secondary">
(<%= @user.family.trial_remaining_days %> days remaining)
</span>
</p>
<% elsif @user.family.subscribed? %>
<p class="text-primary">You are currently subscribed to <span class="font-medium">Maybe+</span></p>
<% else %>
<p class="text-primary">You are currently <span class="font-medium">not subscribed</span></p>
@ -18,7 +31,7 @@
</div>
</div>
<% if @user.family.subscribed? || subscription_pending? %>
<% if @user.family.subscribed? %>
<%= render LinkComponent.new(
text: "Manage",
icon: "external-link",
@ -27,13 +40,13 @@
href: subscription_path,
rel: "noopener"
) %>
<% else %>
<% elsif @user.family.trialing? && !subscription_pending? %>
<%= render LinkComponent.new(
text: "Subscribe",
text: "Choose plan",
variant: "primary",
icon: "external-link",
icon: "plus",
icon_position: "right",
href: new_subscription_path,
href: upgrade_subscription_path(view: "upgrade"),
rel: "noopener") %>
<% end %>
</div>

View file

@ -17,7 +17,11 @@
<div class="grow flex flex-col items-center justify-center">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
<p class="text-xl lg:text-3xl text-primary font-display font-medium">Your trial is over</p>
<% if Current.family.trialing? %>
<p class="text-xl lg:text-3xl text-primary font-display font-medium">Your trial has <%= Current.family.trial_remaining_days %> days remaining</p>
<% else %>
<p class="text-xl lg:text-3xl text-primary font-display font-medium">Your trial is over</p>
<% end %>
<h2 class="text-xl lg:text-3xl font-display font-medium mb-2">
<span class="text-secondary">Unlock</span>

View file

@ -35,7 +35,9 @@ Rails.application.configure do
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = ENV.fetch("ACTIVE_STORAGE_SERVICE", "local").to_sym
config.active_storage.default_url_options = { host: "localhost", port: 3000 }
config.after_initialize do
ActiveStorage::Current.url_options = { host: "localhost", port: 3000 }
end
# Set Active Storage URL expiration time to 7 days
config.active_storage.urls_expire_in = 7.days

View file

@ -5,6 +5,11 @@ namespace :demo_data do
Demo::Generator.new.reset_and_clear_data!(families)
end
task new_user: :environment do
families = [ "Demo Family 1" ]
Demo::Generator.new.reset_and_clear_data!(families, require_onboarding: true)
end
task :reset, [ :count ] => :environment do |t, args|
count = (args[:count] || 1).to_i
families = count.times.map { |i| "Demo Family #{i + 1}" }