From 780dc76dce3a159a2d078a8b38f3b4109bd80830 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Thu, 1 May 2025 16:35:02 -0400 Subject: [PATCH] Fix subscription endpoints --- app/controllers/application_controller.rb | 16 ----------- app/controllers/concerns/onboardable.rb | 10 ++++++- app/controllers/subscriptions_controller.rb | 4 +-- app/models/demo/generator.rb | 12 ++++---- app/models/family.rb | 8 ++++-- app/models/provider/stripe.rb | 6 ++-- app/views/onboardings/show.html.erb | 12 ++++---- app/views/settings/billings/show.html.erb | 31 +++++++++++++++------ app/views/subscriptions/upgrade.html.erb | 6 +++- config/environments/development.rb | 4 ++- lib/tasks/demo_data.rake | 5 ++++ 11 files changed, 68 insertions(+), 46 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ab192b4..44a94d12 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb index d39c228d..804667b4 100644 --- a/app/controllers/concerns/onboardable.rb +++ b/app/controllers/concerns/onboardable.rb @@ -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 diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 4c110dd6..895c7192 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -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 ) diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index 5b096b87..a7ce8940 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -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) diff --git a/app/models/family.rb b/app/models/family.rb index 923a7566..0adb1521 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -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? diff --git a/app/models/provider/stripe.rb b/app/models/provider/stripe.rb index 37d59e0a..09c67911 100644 --- a/app/models/provider/stripe.rb +++ b/app/models/provider/stripe.rb @@ -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 diff --git a/app/views/onboardings/show.html.erb b/app/views/onboardings/show.html.erb index 4f58ac51..9a2837e6 100644 --- a/app/views/onboardings/show.html.erb +++ b/app/views/onboardings/show.html.erb @@ -24,23 +24,25 @@
- <%= 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 %>
<% unless @invitation %>
<%= 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 %>
<% end %> - <%= form.submit t(".submit") %> + <%= form.submit "Continue" %> <% end %> diff --git a/app/views/settings/billings/show.html.erb b/app/views/settings/billings/show.html.erb index 6775d46a..221cdaad 100644 --- a/app/views/settings/billings/show.html.erb +++ b/app/views/settings/billings/show.html.erb @@ -4,12 +4,25 @@
-
- <%= icon "gem" %> -
+ <%= render FilledIconComponent.new( + icon: "gem", + rounded: true, + size: "lg" + ) %>
- <% if @user.family.subscribed? || subscription_pending? %> + <% if subscription_pending? %> +

+ Your subscription is pending. You can still use Maybe+ while we process your subscription. +

+ <% elsif @user.family.trialing? %> +

+ You are currently trialing Maybe+ + + (<%= @user.family.trial_remaining_days %> days remaining) + +

+ <% elsif @user.family.subscribed? %>

You are currently subscribed to Maybe+

<% else %>

You are currently not subscribed

@@ -18,7 +31,7 @@
- <% 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 %>
diff --git a/app/views/subscriptions/upgrade.html.erb b/app/views/subscriptions/upgrade.html.erb index d005fd5a..b66e927b 100644 --- a/app/views/subscriptions/upgrade.html.erb +++ b/app/views/subscriptions/upgrade.html.erb @@ -17,7 +17,11 @@
<%= image_tag "logo-color.png", class: "w-16 mb-6" %> -

Your trial is over

+ <% if Current.family.trialing? %> +

Your trial has <%= Current.family.trial_remaining_days %> days remaining

+ <% else %> +

Your trial is over

+ <% end %>

Unlock diff --git a/config/environments/development.rb b/config/environments/development.rb index 261671b2..553da47e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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 diff --git a/lib/tasks/demo_data.rake b/lib/tasks/demo_data.rake index 2f5a1b6d..c57a006f 100644 --- a/lib/tasks/demo_data.rake +++ b/lib/tasks/demo_data.rake @@ -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}" }