diff --git a/Gemfile.saas b/Gemfile.saas
index 779c8c419..955a66303 100644
--- a/Gemfile.saas
+++ b/Gemfile.saas
@@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "activeresource", require: "active_resource"
gem "queenbee", bc: "queenbee-plugin"
-gem "fizzy-saas", bc: "fizzy-saas"
+gem "fizzy-saas", bc: "fizzy-saas", branch: "extract-signup"
gem "rails_structured_logging", bc: "rails-structured-logging"
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index cc24a9147..b8f6dc0e7 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -1,6 +1,7 @@
GIT
remote: https://github.com/basecamp/fizzy-saas
- revision: 40f68d8604a1b410ba514f5793a93e9b11c00219
+ revision: 3b012b9ffa7c8fdc1e4b9f5aa7d866ed4108a8ff
+ branch: extract-signup
specs:
fizzy-saas (0.1.0)
queenbee
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 89b94f079..7f1cb6565 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -4,7 +4,6 @@ class ApplicationController < ActionController::Base
include CurrentRequest, CurrentTimezone, SetPlatform
include RequestForgeryProtection
include TurboFlash, ViewTransitions
- include Saas
include RoutingHeaders
etag { "v1" }
diff --git a/app/controllers/concerns/saas.rb b/app/controllers/concerns/saas.rb
deleted file mode 100644
index fd4ca8491..000000000
--- a/app/controllers/concerns/saas.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Saas
- extend ActiveSupport::Concern
-
- included do
- helper_method :signups_allowed?
- end
-
- def signups_allowed?
- defined?(Signup) && defined?(saas)
- end
-end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 8c623b6ef..65cb443a0 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -22,10 +22,8 @@ class SessionsController < ApplicationController
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
- elsif signups_allowed?
- Signup.new(email_address: email_address).create_identity
- session[:return_to_after_authenticating] = saas.new_signup_completion_path
- redirect_to session_magic_link_path
+ elsif
+ process_new_signup
end
end
@@ -38,4 +36,10 @@ class SessionsController < ApplicationController
def email_address
params.expect(:email_address)
end
+
+ def process_new_signup
+ Signup.new(email_address: email_address).create_identity
+ session[:return_to_after_authenticating] = new_signup_completion_path
+ redirect_to session_magic_link_path
+ end
end
diff --git a/app/controllers/signup/completions_controller.rb b/app/controllers/signup/completions_controller.rb
new file mode 100644
index 000000000..d7f09c086
--- /dev/null
+++ b/app/controllers/signup/completions_controller.rb
@@ -0,0 +1,24 @@
+class Signup::CompletionsController < ApplicationController
+ layout "public"
+
+ disallow_account_scope
+
+ def new
+ @signup = Signup.new(identity: Current.identity)
+ end
+
+ def create
+ @signup = Signup.new(signup_params)
+
+ if @signup.complete
+ redirect_to landing_url(script_name: @signup.account.slug)
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ private
+ def signup_params
+ params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity)
+ end
+end
diff --git a/app/models/signup.rb b/app/models/signup.rb
new file mode 100644
index 000000000..230aafd0b
--- /dev/null
+++ b/app/models/signup.rb
@@ -0,0 +1,100 @@
+class Signup
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include ActiveModel::Validations
+
+ attr_accessor :full_name, :email_address, :identity
+ attr_reader :account, :user
+
+ with_options on: :completion do
+ validates_presence_of :full_name, :identity
+ end
+
+ def initialize(...)
+ super
+
+ @email_address = @identity.email_address if @identity
+ end
+
+ def create_identity
+ @identity = Identity.find_or_create_by!(email_address: email_address)
+ @identity.send_magic_link
+ end
+
+ def complete
+ if valid?(:completion)
+ begin
+ @tenant = create_tenant
+ create_account
+ true
+ rescue => error
+ destroy_account
+ handle_account_creation_error(error)
+
+ errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.")
+ Rails.error.report(error, severity: :error)
+ Rails.logger.error error
+ Rails.logger.error error.backtrace.join("\n")
+
+ false
+ end
+ else
+ false
+ end
+ end
+
+ private
+ # Override to customize the handling of external accounts associated to the account.
+ def create_tenant
+ nil
+ end
+
+ # Override to inject custom handling for account creation errors
+ def handle_account_creation_error(error)
+ end
+
+ def create_account
+ @account = Account.create_with_admin_user(
+ account: {
+ external_account_id: @tenant,
+ name: generate_account_name
+ },
+ owner: {
+ name: full_name,
+ identity: identity
+ }
+ )
+ @user = @account.users.find_by!(role: :admin)
+ @account.setup_customer_template
+ end
+
+ def generate_account_name
+ AccountNameGenerator.new(identity: identity, name: full_name).generate
+ end
+
+
+ def destroy_account
+ @account&.destroy!
+
+ @user = nil
+ @account = nil
+ @tenant = nil
+ end
+
+ def subscription_attributes
+ subscription = FreeV1Subscription
+
+ {}.tap do |attributes|
+ attributes[:name] = subscription.to_param
+ attributes[:price] = subscription.price
+ end
+ end
+
+ def request_attributes
+ {}.tap do |attributes|
+ attributes[:remote_address] = Current.ip_address
+ attributes[:user_agent] = Current.user_agent
+ attributes[:referrer] = Current.referrer
+ end
+ end
+end
diff --git a/app/models/signup/account_name_generator.rb b/app/models/signup/account_name_generator.rb
new file mode 100644
index 000000000..a6844d3a3
--- /dev/null
+++ b/app/models/signup/account_name_generator.rb
@@ -0,0 +1,53 @@
+class Signup::AccountNameGenerator
+ SUFFIX = "Fizzy".freeze
+
+ attr_reader :identity, :name
+
+ def initialize(identity:, name:)
+ @identity = identity
+ @name = name
+ end
+
+ def generate
+ next_index = current_index + 1
+
+ if next_index == 1
+ "#{prefix} #{SUFFIX}"
+ else
+ "#{prefix} #{next_index.ordinalize} #{SUFFIX}"
+ end
+ end
+
+ private
+ def current_index
+ existing_indices.max || 0
+ end
+
+ def existing_indices
+ Current.without_account do
+ identity.accounts.filter_map do |account|
+ if account.name.match?(first_account_name_regex)
+ 1
+ elsif match = account.name.match(nth_account_name_regex)
+ match[1].to_i
+ end
+ end
+ end
+ end
+
+ def first_account_name_regex
+ @first_account_name_regex ||= /\A#{prefix}\s+#{SUFFIX}\Z/i
+ end
+
+ def nth_account_name_regex
+ @nth_account_name_regex ||= /\A#{prefix}\s+(1st|2nd|3rd|\d+th)\s+#{SUFFIX}/i
+ end
+
+ def prefix
+ @prefix ||= "#{first_name}'s"
+ end
+
+ def first_name
+ name.strip.split(" ", 2).first
+ end
+end
diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb
index 26b3d25bd..8a36261bf 100644
--- a/app/views/sessions/menus/show.html.erb
+++ b/app/views/sessions/menus/show.html.erb
@@ -24,13 +24,11 @@
You don’t have any Fizzy accounts.
<% end %>
- <% if signups_allowed? %>
-
- <%= link_to saas.new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
- Sign up for a new Fizzy account
- <% end %>
-
- <% end %>
+
+ <%= link_to new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
+ Sign up for a new Fizzy account
+ <% end %>
+
<% end %>
diff --git a/app/views/signup/completions/new.html.erb b/app/views/signup/completions/new.html.erb
new file mode 100644
index 000000000..971c39b1d
--- /dev/null
+++ b/app/views/signup/completions/new.html.erb
@@ -0,0 +1,30 @@
+<% @page_title = "Complete your sign-up" %>
+
+