mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-24 15:49:39 +02:00
New onboarding, trials, Stripe integration (#2185)
* New onboarding, trials, Stripe integration * Fix tests * Lint fixes * Fix subscription endpoints
This commit is contained in:
parent
79b4a3769b
commit
a51c4d2cba
53 changed files with 847 additions and 372 deletions
|
@ -2,27 +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
|
||||
return false if Current.family.active_accounts_count <= 3
|
||||
|
||||
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
|
||||
|
|
|
@ -2,16 +2,40 @@ module Onboardable
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :redirect_to_onboarding, if: :needs_onboarding?
|
||||
before_action :require_onboarding_and_upgrade
|
||||
helper_method :subscription_pending?
|
||||
end
|
||||
|
||||
private
|
||||
def redirect_to_onboarding
|
||||
redirect_to onboarding_path
|
||||
# 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
|
||||
|
||||
def needs_onboarding?
|
||||
Current.user && Current.user.onboarded_at.blank? &&
|
||||
!%w[/users /onboarding /sessions].any? { |path| request.path.start_with?(path) }
|
||||
# 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? && !Current.family.trialing?
|
||||
redirect_to upgrade_subscription_path
|
||||
end
|
||||
end
|
||||
|
||||
def redirectable_path?(path)
|
||||
return false if path.starts_with?("/settings")
|
||||
return false if path.starts_with?("/subscription")
|
||||
return false if path.starts_with?("/onboarding")
|
||||
return false if path.starts_with?("/users")
|
||||
|
||||
[
|
||||
new_registration_path,
|
||||
new_session_path,
|
||||
new_password_reset_path,
|
||||
new_email_confirmation_path
|
||||
].exclude?(path)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
class OnboardingsController < ApplicationController
|
||||
layout "wizard"
|
||||
|
||||
before_action :set_user
|
||||
before_action :load_invitation
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def profile
|
||||
def preferences
|
||||
end
|
||||
|
||||
def preferences
|
||||
def trial
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,41 +1,63 @@
|
|||
class SubscriptionsController < ApplicationController
|
||||
before_action :redirect_to_root_if_self_hosted
|
||||
# Disables subscriptions for self hosted instances
|
||||
guard_feature if: -> { self_hosted? }
|
||||
|
||||
# Upgrade page for unsubscribed users
|
||||
def upgrade
|
||||
render layout: "onboardings"
|
||||
end
|
||||
|
||||
def start_trial
|
||||
if Current.family.trial_started_at.present?
|
||||
redirect_to root_path, alert: "You've already started or completed your trial"
|
||||
else
|
||||
Family.transaction do
|
||||
Current.family.update(trial_started_at: Time.current)
|
||||
Current.user.update(onboarded_at: Time.current)
|
||||
end
|
||||
|
||||
redirect_to root_path, notice: "Your trial has started"
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
if Current.family.stripe_customer_id.blank?
|
||||
customer = stripe_client.v1.customers.create(
|
||||
price_map = {
|
||||
monthly: ENV["STRIPE_MONTHLY_PRICE_ID"],
|
||||
annual: ENV["STRIPE_ANNUAL_PRICE_ID"]
|
||||
}
|
||||
|
||||
price_id = price_map[(params[:plan] || :monthly).to_sym]
|
||||
|
||||
unless Current.family.existing_customer?
|
||||
customer = stripe.create_customer(
|
||||
email: Current.family.primary_user.email,
|
||||
metadata: { family_id: Current.family.id }
|
||||
)
|
||||
|
||||
Current.family.update(stripe_customer_id: customer.id)
|
||||
end
|
||||
|
||||
session = stripe_client.v1.checkout.sessions.create({
|
||||
customer: Current.family.stripe_customer_id,
|
||||
line_items: [ {
|
||||
price: ENV["STRIPE_PLAN_ID"],
|
||||
quantity: 1
|
||||
} ],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
checkout_session_url = stripe.get_checkout_session_url(
|
||||
price_id: price_id,
|
||||
customer_id: Current.family.stripe_customer_id,
|
||||
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: settings_billing_url
|
||||
})
|
||||
cancel_url: upgrade_subscription_url(plan: params[:plan])
|
||||
)
|
||||
|
||||
redirect_to session.url, allow_other_host: true, status: :see_other
|
||||
redirect_to checkout_session_url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
|
||||
def show
|
||||
portal_session = stripe_client.v1.billing_portal.sessions.create(
|
||||
customer: Current.family.stripe_customer_id,
|
||||
portal_session_url = stripe.get_billing_portal_session_url(
|
||||
customer_id: Current.family.stripe_customer_id,
|
||||
return_url: settings_billing_url
|
||||
)
|
||||
|
||||
redirect_to portal_session.url, allow_other_host: true, status: :see_other
|
||||
redirect_to portal_session_url, allow_other_host: true, status: :see_other
|
||||
end
|
||||
|
||||
def success
|
||||
checkout_session = stripe_client.v1.checkout.sessions.retrieve(params[:session_id])
|
||||
checkout_session = stripe.retrieve_checkout_session(params[:session_id])
|
||||
Current.session.update(subscribed_at: Time.at(checkout_session.created))
|
||||
redirect_to root_path, notice: "You have successfully subscribed to Maybe+."
|
||||
rescue Stripe::InvalidRequestError
|
||||
|
@ -43,11 +65,7 @@ class SubscriptionsController < ApplicationController
|
|||
end
|
||||
|
||||
private
|
||||
def stripe_client
|
||||
@stripe_client ||= Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
end
|
||||
|
||||
def redirect_to_root_if_self_hosted
|
||||
redirect_to root_path, alert: I18n.t("subscriptions.self_hosted_alert") if self_hosted?
|
||||
def stripe
|
||||
@stripe ||= Provider::Registry.get_provider(:stripe)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -63,6 +63,10 @@ class UsersController < ApplicationController
|
|||
redirect_to root_path
|
||||
when "preferences"
|
||||
redirect_to settings_preferences_path, notice: notice
|
||||
when "goals"
|
||||
redirect_to goals_onboarding_path
|
||||
when "trial"
|
||||
redirect_to trial_onboarding_path
|
||||
else
|
||||
redirect_to settings_profile_path, notice: notice
|
||||
end
|
||||
|
@ -83,8 +87,10 @@ class UsersController < ApplicationController
|
|||
|
||||
def user_params
|
||||
params.require(:user).permit(
|
||||
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme,
|
||||
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ]
|
||||
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
|
||||
:show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme, :set_onboarding_preferences_at, :set_onboarding_goals_at,
|
||||
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id ],
|
||||
goals: []
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -33,61 +33,21 @@ class WebhooksController < ApplicationController
|
|||
end
|
||||
|
||||
def stripe
|
||||
webhook_body = request.body.read
|
||||
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
|
||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||
stripe_provider = Provider::Registry.get_provider(:stripe)
|
||||
|
||||
begin
|
||||
thin_event = client.parse_thin_event(webhook_body, sig_header, ENV["STRIPE_WEBHOOK_SECRET"])
|
||||
webhook_body = request.body.read
|
||||
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
|
||||
|
||||
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
|
||||
stripe_provider.process_webhook_later(webhook_body, sig_header)
|
||||
|
||||
head :ok
|
||||
rescue JSON::ParserError => error
|
||||
Sentry.capture_exception(error)
|
||||
render json: { error: "Invalid payload" }, status: :bad_request
|
||||
return
|
||||
head :bad_request
|
||||
rescue Stripe::SignatureVerificationError => error
|
||||
Sentry.capture_exception(error)
|
||||
render json: { error: "Invalid signature" }, status: :bad_request
|
||||
return
|
||||
head :bad_request
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue