Class: Organizations::JoinState

Inherits:
Object
  • Object
show all
Defined in:
lib/organizations/join_state.rb

Overview

The headless state machine behind any "join this organization" screen — BYO-UI in its truest form: the gem ships the STATE, the host ships the pixels. One object answers "which state is this screen in" so a page body, a pinned CTA, and a Turbo Stream response can never disagree (splitting these predicates across partials is exactly how a submit button ends up pointing at a form that no longer exists — a bug the first production host paid for before extracting this).

:member    — the viewer belongs; show the success/hub state
:verifying — an emailed-code challenge is in flight; show the 6-digit
           input (+ resend with #resend_seconds cooldown)
:pending   — a request awaits manual approval; show the waiting state
:entry     — no relationship yet; show the join form(s) the org's
           instruments allow (organization.accepts_domain_joining? /
           accepts_code_joining? / accepts_join_requests?)

Examples:

In a controller

@result = Organizations::JoinFlow.attempt(user: current_user, organization: @org, **join_params)
@state  = Organizations::JoinState.for(user: current_user, organization: @org, result: @result)

In the view

case @state.status
when :member    then render "joined"
when :verifying then render "code_input", seconds: @state.resend_seconds
when :pending   then render "waiting"
when :entry     then render "join_form"
end

Constant Summary collapse

STATUSES =
%i[member verifying pending entry].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(organization:, membership:, join_request:, result: nil) ⇒ JoinState

Returns a new instance of JoinState.



54
55
56
57
58
59
# File 'lib/organizations/join_state.rb', line 54

def initialize(organization:, membership:, join_request:, result: nil)
  @organization = organization
  @membership = membership
  @join_request = join_request
  @result = result
end

Instance Attribute Details

#join_requestObject (readonly)

Returns the value of attribute join_request.



34
35
36
# File 'lib/organizations/join_state.rb', line 34

def join_request
  @join_request
end

#membershipObject (readonly)

Returns the value of attribute membership.



34
35
36
# File 'lib/organizations/join_state.rb', line 34

def membership
  @membership
end

#organizationObject (readonly)

Returns the value of attribute organization.



34
35
36
# File 'lib/organizations/join_state.rb', line 34

def organization
  @organization
end

#resultObject (readonly)

Returns the value of attribute result.



34
35
36
# File 'lib/organizations/join_state.rb', line 34

def result
  @result
end

Class Method Details

.for(user:, organization:, result: nil) ⇒ JoinState

Build the state for a viewer. Pass the JoinFlow::Result of the action that JUST ran (nil on a plain GET): its records are fresher than any association cache — right after a successful verify, the new membership lives on the result, not necessarily in a reloaded association.

Parameters:

Returns:



45
46
47
48
49
50
51
52
# File 'lib/organizations/join_state.rb', line 45

def self.for(user:, organization:, result: nil)
  new(
    organization: organization,
    membership: result&.membership || organization.memberships.find_by(user_id: user.id),
    join_request: result&.join_request || user.pending_join_request_for(organization),
    result: result
  )
end

Instance Method Details

#challengeOrganizations::JoinRequest?

The join request whose emailed challenge is being answered (feeds the verify form and #resend_seconds).

Returns:



94
95
96
# File 'lib/organizations/join_state.rb', line 94

def challenge
  result&.join_request || join_request
end

#entry?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/organizations/join_state.rb', line 87

def entry?
  status == :entry
end

#error_messageString?

Localized message of a just-failed action, nil otherwise.

Returns:

  • (String, nil)


113
114
115
# File 'lib/organizations/join_state.rb', line 113

def error_message
  result&.message if result&.failed?
end

#member?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/organizations/join_state.rb', line 70

def member?
  membership.present? || result&.member?
end

#pending?Boolean

Returns:

  • (Boolean)


81
82
83
84
85
# File 'lib/organizations/join_state.rb', line 81

def pending?
  return false if member? || verifying?

  join_request.present? || result&.pending?
end

#resend_secondsInteger

Seconds until the gem will accept another code send for this challenge — drive a client-side resend cooldown from this instead of mirroring config.verification_resend_interval as a magic number in the host (mirrored constants desync the day someone tunes the config).

Returns:

  • (Integer)

    0 when a resend is allowed now



103
104
105
106
107
108
109
# File 'lib/organizations/join_state.rb', line 103

def resend_seconds
  sent_at = challenge&.verification_sent_at
  return 0 if sent_at.blank?

  interval = Organizations.configuration.verification_resend_interval.to_i
  [interval - (Time.current - sent_at).to_i, 0].max
end

#statusSymbol

Returns one of STATUSES.

Returns:

  • (Symbol)

    one of STATUSES



62
63
64
65
66
67
68
# File 'lib/organizations/join_state.rb', line 62

def status
  return :member if member?
  return :verifying if verifying?
  return :pending if pending?

  :entry
end

#verifying?Boolean

Returns:

  • (Boolean)


74
75
76
77
78
79
# File 'lib/organizations/join_state.rb', line 74

def verifying?
  return false if member?
  return true if result&.challenge_sent?

  challenge.present? && challenge.verification_sent_at.present? && !challenge.email_verified?
end