Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session extension modal properly appears after 5 seconds (for develop… #469

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
16 changes: 16 additions & 0 deletions app/app/controllers/cbv/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Cbv
class SessionsController < ApplicationController
def extend
# Reset the Devise timeout timer
request.env['devise.skip_timeout'] = true
current_user.try(:remember_me!)
current_user.try(:remember_me=, true)
sign_in(current_user, force: true)

respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.remove("session-timeout-modal") }
format.html { redirect_back(fallback_location: root_path) }
end
end
end
end
143 changes: 143 additions & 0 deletions app/app/javascript/controllers/cbv/sessions/timeout_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["modal"]
static values = {
warningTime: { type: Number, default: 5000 },
// warningTime: { type: Number, default: 25 * 60 * 1000 }, // 25 minutes in milliseconds
timeoutTime: { type: Number, default: 30 * 60 * 1000 } // 30 minutes in milliseconds
}

connect() {
console.log('Timeout controller connected')
console.log('Warning will show in:', this.warningTimeValue, 'ms')
console.log('Timeout will occur in:', this.timeoutTimeValue, 'ms')
this.createModalTrigger()
this.resetTimers()
this.addActivityListeners()
}

disconnect() {
console.log('Timeout controller disconnected')
this.clearTimers()
this.removeActivityListeners()
this.removeTriggerButton()
}

resetTimers() {
console.log('Resetting timers')
this.clearTimers()
console.log('Setting warning timer for', this.warningTimeValue, 'ms')
this.warningTimer = setTimeout(() => {
console.log('Warning timer triggered')
this.showWarning()
}, this.warningTimeValue)

console.log('Setting timeout timer for', this.timeoutTimeValue, 'ms')
this.timeoutTimer = setTimeout(() => {
console.log('Timeout timer triggered')
this.timeout()
}, this.timeoutTimeValue)
}

clearTimers() {
if (this.warningTimer) {
console.log('Clearing warning timer')
clearTimeout(this.warningTimer)
}
if (this.timeoutTimer) {
console.log('Clearing timeout timer')
clearTimeout(this.timeoutTimer)
}
if (this.activityTimeout) {
console.log('Clearing activity timeout')
clearTimeout(this.activityTimeout)
}
}

addActivityListeners() {
console.log('Adding activity listeners')
this.boundHandleActivity = this.handleActivity.bind(this)
;['mousedown', 'keydown', 'touchstart', 'scroll'].forEach(eventName => {
document.addEventListener(eventName, this.boundHandleActivity, true)
})
}

removeActivityListeners() {
console.log('Removing activity listeners')
if (this.boundHandleActivity) {
;['mousedown', 'keydown', 'touchstart', 'scroll'].forEach(eventName => {
document.removeEventListener(eventName, this.boundHandleActivity, true)
})
}
}

handleActivity() {
console.log('Activity detected')

// Clear any pending activity timeout
if (this.activityTimeout) {
clearTimeout(this.activityTimeout)
}

// Wait 1 second after last activity before resetting timers
this.activityTimeout = setTimeout(() => {
console.log('No activity for 1 second, resetting timers')
this.resetTimers()
}, 1000)
}

createModalTrigger() {
// Remove any existing trigger
this.removeTriggerButton()

// Create the trigger link matching the help link structure
const trigger = document.createElement('a')
trigger.id = 'session-timeout-trigger'
trigger.href = '#cbv-session-timeout-content'
trigger.className = 'usa-button'
trigger.setAttribute('aria-controls', 'cbv-session-timeout-content')
trigger.dataset.openModal = true // Set to true instead of empty string
trigger.textContent = 'Open Session Timeout Modal'

// Add to DOM in a visible location
const container = document.createElement('div')
container.style.position = 'fixed'
container.style.top = '20px'
container.style.right = '20px'
container.style.zIndex = '9999'
container.appendChild(trigger)
document.body.appendChild(container)
console.log('Created modal trigger:', trigger)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would try to put these in the layout template instead of creating/tearing down each time. There are turbo features for selecting stuff like this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure! This is preliminary stuff used for development, but if we opt to use a hidden button then I'll be sure to move it to the template.

}

removeTriggerButton() {
const existingTrigger = document.getElementById('session-timeout-trigger')
if (existingTrigger) {
const container = existingTrigger.parentElement
if (container) {
container.remove()
} else {
existingTrigger.remove()
}
console.log('Removed existing trigger button')
}
}

showWarning() {
console.log('Attempting to show warning modal')
const triggerButton = document.getElementById('session-timeout-trigger')
if (!triggerButton) {
console.error('Modal trigger button not found')
return
}

console.log('Clicking modal trigger button')
triggerButton.click()
}

timeout() {
console.log('Session timed out, redirecting to root')
window.location = '/'
}
}
2 changes: 2 additions & 0 deletions app/app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { application } from "./application"

import CbvEmployerSearch from "./cbv/employer_search"
import CbvSynchronizationsController from "./cbv/synchronizations_controller"
import CbvSessionsTimeoutController from "./cbv/sessions/timeout_controller"
import HelpController from "./help"

application.register("cbv-employer-search", CbvEmployerSearch)
application.register("cbv-synchronizations", CbvSynchronizationsController)
application.register("cbv-sessions-timeout", CbvSessionsTimeoutController)
application.register("help", HelpController)
24 changes: 24 additions & 0 deletions app/app/views/cbv/sessions/_timeout_modal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="usa-modal" id="cbv-session-timeout-content" aria-labelledby="cbv-session-timeout-heading" aria-describedby="cbv-session-timeout-description">
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="cbv-session-timeout-heading">
<%= t('session_timeout.modal.heading') %>
</h2>
<div class="usa-prose">
<p id="cbv-session-timeout-description">
<%= t('session_timeout.modal.description') %>
</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<%= button_to t('session_timeout.modal.extend_button'), cbv_flow_extend_session_path, class: "usa-button", data: { turbo: true } %>
</li>
<li class="usa-button-group__item">
<%= button_to t('session_timeout.modal.end_button'), destroy_user_session_path, method: :delete, class: "usa-button usa-button--outline" %>
</li>
</ul>
</div>
</div>
</div>
</div>
6 changes: 5 additions & 1 deletion app/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</head>

<body>
<div id="root">
<div id="root" data-controller="cbv-sessions-timeout">
<a class="usa-skipnav" href="#main-content"><%= t("shared.skip_link") %></a>
<%= render "application/header" %>
<main id="main-content">
Expand Down Expand Up @@ -54,6 +54,10 @@
</section>
</main>
<%= render "application/footer" %>

<div data-cbv-sessions-timeout-target="modal" class="hidden">
<%= render "cbv/sessions/timeout_modal" %>
</div>
</div>

<%= render partial: "help/help_modal", locals: { help_path: help_path(locale: I18n.locale, r: SecureRandom.hex(4)) } %>
Expand Down
6 changes: 6 additions & 0 deletions app/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,9 @@ en:
users:
omniauth_callbacks:
authentication_successful: You are signed in.
session_timeout:
modal:
heading: "Do you need more time?"
description: "It looks like you're inactive. For your security, we will sign you out soon unless you choose to extend your session."
extend_button: "Yes, extend my session"
end_button: "End session"
1 change: 1 addition & 0 deletions app/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
resource :add_job, only: %i[show create]
resource :payment_details, only: %i[show update]
resource :expired_invitation, only: %i[show]
post 'extend_session', to: 'sessions#extend', as: :extend_session

# Utility route to clear your session; useful during development
resource :reset, only: %i[show]
Expand Down
Loading