Skip to content

Commit 0049494

Browse files
committed
Bring simple signup flow from the fizzy-saas gem
We skip the QB code and we fill external account ids automatically on creation with a sequence See: basecamp/fizzy-saas#7
1 parent dea2bd4 commit 0049494

19 files changed

+443
-34
lines changed

Gemfile.saas

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
66

77
gem "activeresource", require: "active_resource"
88
gem "queenbee", bc: "queenbee-plugin"
9-
gem "fizzy-saas", bc: "fizzy-saas"
9+
gem "fizzy-saas", bc: "fizzy-saas", branch: "extract-signup"
1010
gem "rails_structured_logging", bc: "rails-structured-logging"
1111

Gemfile.saas.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
GIT
22
remote: https://github.com/basecamp/fizzy-saas
3-
revision: 40f68d8604a1b410ba514f5793a93e9b11c00219
3+
revision: 3b012b9ffa7c8fdc1e4b9f5aa7d866ed4108a8ff
4+
branch: extract-signup
45
specs:
56
fizzy-saas (0.1.0)
67
queenbee

app/controllers/application_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ class ApplicationController < ActionController::Base
44
include CurrentRequest, CurrentTimezone, SetPlatform
55
include RequestForgeryProtection
66
include TurboFlash, ViewTransitions
7-
include Saas
87
include RoutingHeaders
98

109
etag { "v1" }

app/controllers/concerns/saas.rb

Lines changed: 0 additions & 11 deletions
This file was deleted.

app/controllers/sessions_controller.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ def create
2222
magic_link = identity.send_magic_link
2323
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
2424
redirect_to session_magic_link_path
25-
elsif signups_allowed?
26-
Signup.new(email_address: email_address).create_identity
27-
session[:return_to_after_authenticating] = saas.new_signup_completion_path
28-
redirect_to session_magic_link_path
25+
elsif
26+
process_new_signup
2927
end
3028
end
3129

@@ -38,4 +36,10 @@ def destroy
3836
def email_address
3937
params.expect(:email_address)
4038
end
39+
40+
def process_new_signup
41+
Signup.new(email_address: email_address).create_identity
42+
session[:return_to_after_authenticating] = new_signup_completion_path
43+
redirect_to session_magic_link_path
44+
end
4145
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class Signup::CompletionsController < ApplicationController
2+
layout "public"
3+
4+
disallow_account_scope
5+
6+
def new
7+
@signup = Signup.new(identity: Current.identity)
8+
end
9+
10+
def create
11+
@signup = Signup.new(signup_params)
12+
13+
if @signup.complete
14+
redirect_to landing_url(script_name: @signup.account.slug)
15+
else
16+
render :new, status: :unprocessable_entity
17+
end
18+
end
19+
20+
private
21+
def signup_params
22+
params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity)
23+
end
24+
end

app/models/signup.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
class Signup
2+
include ActiveModel::Model
3+
include ActiveModel::Attributes
4+
include ActiveModel::Validations
5+
6+
attr_accessor :full_name, :email_address, :identity
7+
attr_reader :account, :user
8+
9+
with_options on: :completion do
10+
validates_presence_of :full_name, :identity
11+
end
12+
13+
def initialize(...)
14+
super
15+
16+
@email_address = @identity.email_address if @identity
17+
end
18+
19+
def create_identity
20+
@identity = Identity.find_or_create_by!(email_address: email_address)
21+
@identity.send_magic_link
22+
end
23+
24+
def complete
25+
if valid?(:completion)
26+
begin
27+
@tenant = create_tenant
28+
create_account
29+
true
30+
rescue => error
31+
destroy_account
32+
handle_account_creation_error(error)
33+
34+
errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.")
35+
Rails.error.report(error, severity: :error)
36+
Rails.logger.error error
37+
Rails.logger.error error.backtrace.join("\n")
38+
39+
false
40+
end
41+
else
42+
false
43+
end
44+
end
45+
46+
private
47+
# Override to customize the handling of external accounts associated to the account.
48+
def create_tenant
49+
nil
50+
end
51+
52+
# Override to inject custom handling for account creation errors
53+
def handle_account_creation_error(error)
54+
end
55+
56+
def create_account
57+
@account = Account.create_with_admin_user(
58+
account: {
59+
external_account_id: @tenant,
60+
name: generate_account_name
61+
},
62+
owner: {
63+
name: full_name,
64+
identity: identity
65+
}
66+
)
67+
@user = @account.users.find_by!(role: :admin)
68+
@account.setup_customer_template
69+
end
70+
71+
def generate_account_name
72+
AccountNameGenerator.new(identity: identity, name: full_name).generate
73+
end
74+
75+
76+
def destroy_account
77+
@account&.destroy!
78+
79+
@user = nil
80+
@account = nil
81+
@tenant = nil
82+
end
83+
84+
def subscription_attributes
85+
subscription = FreeV1Subscription
86+
87+
{}.tap do |attributes|
88+
attributes[:name] = subscription.to_param
89+
attributes[:price] = subscription.price
90+
end
91+
end
92+
93+
def request_attributes
94+
{}.tap do |attributes|
95+
attributes[:remote_address] = Current.ip_address
96+
attributes[:user_agent] = Current.user_agent
97+
attributes[:referrer] = Current.referrer
98+
end
99+
end
100+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class Signup::AccountNameGenerator
2+
SUFFIX = "Fizzy".freeze
3+
4+
attr_reader :identity, :name
5+
6+
def initialize(identity:, name:)
7+
@identity = identity
8+
@name = name
9+
end
10+
11+
def generate
12+
next_index = current_index + 1
13+
14+
if next_index == 1
15+
"#{prefix} #{SUFFIX}"
16+
else
17+
"#{prefix} #{next_index.ordinalize} #{SUFFIX}"
18+
end
19+
end
20+
21+
private
22+
def current_index
23+
existing_indices.max || 0
24+
end
25+
26+
def existing_indices
27+
Current.without_account do
28+
identity.accounts.filter_map do |account|
29+
if account.name.match?(first_account_name_regex)
30+
1
31+
elsif match = account.name.match(nth_account_name_regex)
32+
match[1].to_i
33+
end
34+
end
35+
end
36+
end
37+
38+
def first_account_name_regex
39+
@first_account_name_regex ||= /\A#{prefix}\s+#{SUFFIX}\Z/i
40+
end
41+
42+
def nth_account_name_regex
43+
@nth_account_name_regex ||= /\A#{prefix}\s+(1st|2nd|3rd|\d+th)\s+#{SUFFIX}/i
44+
end
45+
46+
def prefix
47+
@prefix ||= "#{first_name}'s"
48+
end
49+
50+
def first_name
51+
name.strip.split(" ", 2).first
52+
end
53+
end

app/views/sessions/menus/show.html.erb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@
2424
<p class="margin-none-block-start">You don’t have any Fizzy accounts.</p>
2525
<% end %>
2626

27-
<% if signups_allowed? %>
28-
<div class="margin-block-start">
29-
<%= link_to saas.new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
30-
<span>Sign up for a new Fizzy account</span>
31-
<% end %>
32-
</div>
33-
<% end %>
27+
<div class="margin-block-start">
28+
<%= link_to new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
29+
<span>Sign up for a new Fizzy account</span>
30+
<% end %>
31+
</div>
3432
</div>
3533
<% end %>
3634

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<% @page_title = "Complete your sign-up" %>
2+
3+
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
4+
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
5+
6+
<%= form_with model: @signup, url: signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
7+
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %>
8+
9+
<p>You're one step away. Just enter your name to get your own Fizzy account.</p>
10+
11+
<% if @signup.errors.any? %>
12+
<div class="margin-block-half">
13+
<ul class="margin-block-none txt-negative txt-small">
14+
<% @signup.errors.full_messages.each do |message| %>
15+
<li><%= message %></li>
16+
<% end %>
17+
</ul>
18+
</div>
19+
<% end %>
20+
21+
<button type="submit" class="btn btn--link center" data-form-target="submit">
22+
<span>Continue</span>
23+
<%= icon_tag "arrow-right" %>
24+
</button>
25+
<% end %>
26+
</div>
27+
28+
<% content_for :footer do %>
29+
<%= render "sessions/footer" %>
30+
<% end %>

0 commit comments

Comments
 (0)