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:
parent
1638c1e68a
commit
780dc76dce
11 changed files with 68 additions and 46 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue