diff --git a/.env b/.env new file mode 100644 index 000000000..caecd67db --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +ARGYLE_API_TOKEN= +ARGYLE_SANDBOX= +ARGYLE_WEBHOOK_SECRET= +NGROK_URL= diff --git a/Gemfile.lock b/Gemfile.lock index 6f7c6ad1e..62e9e0706 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,6 +162,8 @@ GEM nio4r (2.7.1) nokogiri (1.16.4-arm64-darwin) racc (~> 1.4) + nokogiri (1.16.4-x86_64-darwin) + racc (~> 1.4) nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) parallel (1.24.0) @@ -336,6 +338,7 @@ GEM PLATFORMS arm64-darwin-23 + x86_64-darwin-20 x86_64-linux DEPENDENCIES diff --git a/README.md b/README.md index d7c128a37..bdbcb4cbd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ guide for an introduction to the framework. * Chromedriver must be allowed to run. You can either do that by: * The command line: `xattr -d com.apple.quarantine $(which chromedriver)` (this is the only option if you are on Big Sur) * Manually: clicking "allow" when you run the integration tests for the first time and a dialogue opens up + * [Ngrok](https://ngrok.com/download): brew install ngrok/ngrok/ngrok + * Sign up for an account: https://dashboard.ngrok.com/signup + * run `ngrok config add-authtoken {token goes here}` * Set up rbenv and nodenv: * `echo 'if which nodenv >/dev/null 2>/dev/null; then eval "$(nodenv init -)"; fi' >> ~/.zshrc` * `echo 'if which rbenv >/dev/null 2>/dev/null; then eval "$(rbenv init -)"; fi' >> ~/.zshrc` @@ -59,6 +62,8 @@ of the test. To run locally, use `bin/dev` +Separately, run `ngrok 3000`. Copy the Forwarding URL into your .env.local value for `NGROK_URL`. + ## Security ### Authentication diff --git a/app/assets/stylesheets/application.postcss.css b/app/assets/stylesheets/application.postcss.css index 07eaf25fc..c49a99dc0 100644 --- a/app/assets/stylesheets/application.postcss.css +++ b/app/assets/stylesheets/application.postcss.css @@ -1,3 +1,18 @@ /* Entry point for your PostCSS build */ @forward "uswds-settings.scss"; @forward "uswds-components.scss"; + +.argyle-loading { + position: relative; +} + +.argyle-loading::after { + background-color: rgba(0, 0, 0, 0.128); + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 9999; +} diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f4..f78d816c3 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,7 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + def session + @request.session + end end end diff --git a/app/channels/argyle_paystubs_channel.rb b/app/channels/argyle_paystubs_channel.rb new file mode 100644 index 000000000..5c44105de --- /dev/null +++ b/app/channels/argyle_paystubs_channel.rb @@ -0,0 +1,10 @@ +class ArgylePaystubsChannel < ApplicationCable::Channel + def subscribed + cbv_flow = CbvFlow.find(connection.session[:cbv_flow_id]) + stream_for cbv_flow + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +end diff --git a/app/controllers/cbv_flows_controller.rb b/app/controllers/cbv_flows_controller.rb index 4e57fc3d8..8ef262285 100644 --- a/app/controllers/cbv_flows_controller.rb +++ b/app/controllers/cbv_flows_controller.rb @@ -1,7 +1,7 @@ class CbvFlowsController < ApplicationController USER_TOKEN_ENDPOINT = 'https://api-sandbox.argyle.com/v2/users'; ITEMS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/items'; - PAYSTUBS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/paystubs?limit=2&user=' + PAYSTUBS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/paystubs?user=' before_action :set_cbv_flow @@ -58,9 +58,9 @@ def next_path def fetch_and_store_argyle_token return session[:argyle_user_token] if session[:argyle_user_token].present? - raise "ARGYLE_API_TOKEN environment variable is blank. Make sure you have the .env.local.local from 1Password." if ENV['ARGYLE_API_TOKEN'].blank? + raise "ARGYLE_API_TOKEN environment variable is blank. Make sure you have the .env.local.local from 1Password." if Rails.application.credentials.argyle[:api_key].blank? - res = Net::HTTP.post(URI.parse(USER_TOKEN_ENDPOINT), "", {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"}) + res = Net::HTTP.post(URI.parse(USER_TOKEN_ENDPOINT), "", {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) parsed = JSON.parse(res.body) raise "Argyle API error: #{parsed['detail']}" if res.code.to_i >= 400 @@ -71,14 +71,14 @@ def fetch_and_store_argyle_token end def fetch_employers - res = Net::HTTP.get(URI.parse(ITEMS_ENDPOINT), {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"}) + res = Net::HTTP.get(URI.parse(ITEMS_ENDPOINT), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) parsed = JSON.parse(res) parsed['results'] end def fetch_payroll - res = Net::HTTP.get(URI.parse("#{PAYSTUBS_ENDPOINT}#{@cbv_flow.argyle_user_id}"), {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"}) + res = Net::HTTP.get(URI.parse("#{PAYSTUBS_ENDPOINT}#{@cbv_flow.argyle_user_id}"), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"}) parsed = JSON.parse(res) parsed['results'] diff --git a/app/controllers/webhooks/argyle/events_controller.rb b/app/controllers/webhooks/argyle/events_controller.rb new file mode 100644 index 000000000..f9e95c052 --- /dev/null +++ b/app/controllers/webhooks/argyle/events_controller.rb @@ -0,0 +1,20 @@ +class Webhooks::Argyle::EventsController < ApplicationController + skip_before_action :verify_authenticity_token + + def create + signature = OpenSSL::HMAC.hexdigest('SHA512', ENV['ARGYLE_WEBHOOK_SECRET'], request.raw_post) + + unless request.headers["X-Argyle-Signature"] == signature + return render json: { error: 'Invalid signature' }, status: :unauthorized + end + + if params['event'] == 'paystubs.fully_synced' || params['event'] == 'paystubs.partially_synced' + @cbv_flow = CbvFlow.find_by_argyle_user_id(params['data']['user']) + + if @cbv_flow + @cbv_flow.update(payroll_data_available_from: params['data']['available_from']) + ArgylePaystubsChannel.broadcast_to(@cbv_flow, params) + end + end + end +end diff --git a/app/helpers/webhooks/argyle/events_helper.rb b/app/helpers/webhooks/argyle/events_helper.rb new file mode 100644 index 000000000..4f45aa0e6 --- /dev/null +++ b/app/helpers/webhooks/argyle/events_helper.rb @@ -0,0 +1,2 @@ +module Webhooks::Argyle::EventsHelper +end diff --git a/app/javascript/controllers/cbv_flows_controller.js b/app/javascript/controllers/cbv_flows_controller.js index 63ad46acf..cfa3f4003 100644 --- a/app/javascript/controllers/cbv_flows_controller.js +++ b/app/javascript/controllers/cbv_flows_controller.js @@ -1,4 +1,5 @@ import { Controller } from "@hotwired/stimulus" +import * as ActionCable from '@rails/actioncable' import metaContent from "../utilities/meta"; import { loadArgyle, initializeArgyle } from "../utilities/argyle" @@ -8,7 +9,8 @@ function toOptionHTML({ value }) { } export default class extends Controller { - static targets = ["options", "continue", "userAccountId"]; + static targets = ["options", "continue", "userAccountId", "fullySynced", "form"]; + static classes = ["loading"] selection = null; @@ -16,15 +18,36 @@ export default class extends Controller { argyleUserToken = null; + cable = ActionCable.createConsumer(); + + // TODO: information stored on the CbvFlow model can infer whether the paystubs are sync'd + // by checking the value of payroll_data_available_from. We should make that the initial value. + fullySynced = false; + connect() { // check for this value when connected this.argyleUserToken = metaContent('argyle_user_token'); + this.cable.subscriptions.create({ channel: 'ArgylePaystubsChannel' }, { + connected: () => { + console.log("Connected to the channel:", this); + }, + disconnected: () => { + console.log("Disconnected"); + }, + received: (data) => { + console.log("Received some data:", data); + if (data.event === 'paystubs.fully_synced' || data.event === 'paystubs.partially_synced') { + this.fullySynced = true; + + this.formTarget.submit(); + } + } + }); } onSignInSuccess(event) { this.userAccountIdTarget.value = event.accountId; - - this.element.submit(); + this.element.classList.add(this.loadingClass); } onAccountError(event) { diff --git a/app/views/cbv_flows/employer_search.html.erb b/app/views/cbv_flows/employer_search.html.erb index 51c9b2a6a..b5a0d2752 100644 --- a/app/views/cbv_flows/employer_search.html.erb +++ b/app/views/cbv_flows/employer_search.html.erb @@ -5,23 +5,25 @@