From 7dbaaab55c45e1a1ae748a7144c44fdcd38d5129 Mon Sep 17 00:00:00 2001 From: Dave Bellawatt <17937472+dcordz@users.noreply.github.com> Date: Mon, 26 Aug 2024 08:59:29 -0400 Subject: [PATCH] inertia - add maryland svg, fix census query, add unique index to bill.external_id --- .husky/pre-commit | 21 ++ .rubocop.yml | 22 ++ .rubocop/rails.yml | 198 ++++++++++++++++++ .rubocop/rspec.yml | 56 +++++ .rubocop/strict.yml | 19 ++ Gemfile | 89 ++++---- Gemfile.lock | 60 ++++-- app/channels/application_cable/channel.rb | 2 + app/channels/application_cable/connection.rb | 2 + .../admin/bills/creator_controller.rb | 14 +- app/controllers/admin_controller.rb | 2 + app/controllers/application_controller.rb | 105 +++++----- .../bill_of_the_week_controller.rb | 1 + .../bill_score_districts_controller.rb | 5 +- app/controllers/bill_scores_controller.rb | 3 +- app/controllers/bills_controller.rb | 47 +++-- app/controllers/buckets/assets_controller.rb | 52 ++--- app/controllers/concerns/authentication.rb | 13 +- app/controllers/concerns/relying_party.rb | 5 +- app/controllers/concerns/sway_props.rb | 7 +- app/controllers/districts_controller.rb | 4 +- app/controllers/home_controller.rb | 2 +- app/controllers/influence_controller.rb | 5 +- app/controllers/invites_controller.rb | 1 + .../legislator_votes_controller.rb | 3 +- app/controllers/legislators_controller.rb | 3 +- ...h_notification_subscriptions_controller.rb | 77 +++---- .../push_notifications_controller.rb | 43 ++-- app/controllers/notifications_controller.rb | 4 +- .../organization_bill_positions_controller.rb | 6 +- app/controllers/organizations_controller.rb | 15 +- .../phone_verification_controller.rb | 25 +-- app/controllers/sway_locales_controller.rb | 3 +- .../sway_registration_controller.rb | 2 +- app/controllers/user_districts_controller.rb | 5 +- .../user_legislator_scores_controller.rb | 5 +- .../user_legislators_controller.rb | 3 +- .../users/webauthn/registration_controller.rb | 170 +++++++-------- .../users/webauthn/sessions_controller.rb | 152 +++++++------- app/controllers/users/webauthn_controller.rb | 6 +- app/controllers/users_controller.rb | 2 + app/frontend/components/Layout.tsx | 2 - .../admin/creator/BillCreatorFields.tsx | 6 +- .../components/drawer/NoUserAppDrawer.tsx | 58 ----- .../legislator/LegislatorCardSocialItem.tsx | 4 +- .../components/legislator/LegislatorEmail.tsx | 10 +- .../components/legislator/LegislatorPhone.tsx | 3 +- .../components/legislator/LegislatorRoute.tsx | 7 - .../legislator/LegislatorTwitter.tsx | 7 +- app/frontend/components/shared/FlexRowDiv.tsx | 59 ------ .../settings/UserCongratulationsSettings.tsx | 60 ------ .../authentication/useWebAuthnRegistration.ts | 5 +- app/frontend/hooks/useAwardsCount.ts | 68 ------ app/frontend/hooks/useUserVote.ts | 29 --- app/frontend/pages/Bills.tsx | 1 - app/frontend/pages/Influence.tsx | 1 - app/frontend/sway_constants/index.ts | 3 - app/frontend/sway_utils/bills.ts | 15 -- app/frontend/sway_utils/charts.ts | 3 +- app/frontend/sway_utils/index.ts | 8 +- app/helpers/admin/bills/creator_helper.rb | 8 +- app/helpers/application_helper.rb | 2 + app/helpers/bill_of_the_week_helper.rb | 2 + app/helpers/bill_score_districts_helper.rb | 2 + app/helpers/bill_scores_helper.rb | 2 + app/helpers/bills_helper.rb | 2 + app/helpers/buckets/assets_helper.rb | 6 +- app/helpers/districts_helper.rb | 2 + app/helpers/influence_helper.rb | 2 + app/helpers/invites_helper.rb | 2 + app/helpers/legislator_votes_helper.rb | 2 + app/helpers/legislators_helper.rb | 2 + app/helpers/notifications/push_helper.rb | 6 +- .../push_notifications_helper.rb | 6 +- app/helpers/notifications_helper.rb | 2 + .../organization_bill_positions_helper.rb | 2 + app/helpers/organizations_helper.rb | 2 + app/helpers/phone_verification_helper.rb | 2 + app/helpers/registration_helper.rb | 2 + app/helpers/sway_locales_helper.rb | 2 + app/helpers/user_districts_helper.rb | 2 + app/helpers/user_invites_helper.rb | 2 + app/helpers/user_legislator_scores_helper.rb | 2 + app/helpers/user_legislators_helper.rb | 2 + app/helpers/user_votes_helper.rb | 2 + .../users/webauthn/authentication_helper.rb | 8 +- app/helpers/users/webauthn/passkeys_helper.rb | 8 +- .../users/webauthn/registration_helper.rb | 8 +- app/helpers/users/webauthn/sessions_helper.rb | 8 +- app/helpers/users/webauthn_helper.rb | 6 +- app/helpers/users_helper.rb | 2 + app/jobs/application_job.rb | 2 + app/mailers/application_mailer.rb | 2 + app/models/address.rb | 6 +- app/models/application_record.rb | 3 +- app/models/bill.rb | 49 ++--- app/models/bill_cosponsor.rb | 2 + app/models/bill_score.rb | 7 +- app/models/bill_score_district.rb | 1 + app/models/bill_sponsor.rb | 2 + app/models/concerns/agreeable.rb | 28 ++- app/models/concerns/scoreable.rb | 9 +- app/models/concerns/supportable.rb | 16 +- app/models/district.rb | 5 +- app/models/invite.rb | 6 +- app/models/legislator.rb | 29 ++- app/models/legislator_district_score.rb | 2 +- app/models/legislator_vote.rb | 35 ++-- app/models/organization.rb | 5 +- app/models/organization_bill_position.rb | 4 +- app/models/passkey.rb | 2 +- app/models/push_notification_subscription.rb | 15 +- app/models/sway_locale.rb | 21 +- app/models/user.rb | 17 +- app/models/user_address.rb | 2 + app/models/user_district.rb | 2 + app/models/user_inviter.rb | 3 +- app/models/user_legislator.rb | 3 +- app/models/user_legislator_score.rb | 1 + app/models/user_vote.rb | 7 +- app/models/vote.rb | 1 + .../scraper/congress/house/api_connector.rb | 20 +- .../congress/house/legislator_votes.rb | 52 ++--- app/scraper/scraper/congress/house/vote.rb | 6 +- .../scraper/congress/senate/api_connector.rb | 21 +- .../congress/senate/legislator_votes.rb | 135 ++++++------ app/scraper/scraper/congress/senate/vote.rb | 8 +- app/scraper/scraper/congress/senate/votes.rb | 4 +- app/scraper/scraper/faraday_connector.rb | 50 ++--- ...congress_legislator_vote_update_service.rb | 10 +- app/services/influence_service.rb | 4 +- app/services/score_updater_service.rb | 4 +- .../sway_push_notification_service.rb | 3 +- app/services/sway_registration_service.rb | 8 +- config/application.rb | 20 +- config/environments/development.rb | 21 +- ...dd_unique_index_by_external_id_to_bills.rb | 5 + db/schema.rb | 3 +- gemfiles/rubocop.gemfile | 13 ++ package-lock.json | 17 ++ package.json | 4 +- 141 files changed, 1337 insertions(+), 1074 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .rubocop.yml create mode 100644 .rubocop/rails.yml create mode 100644 .rubocop/rspec.yml create mode 100644 .rubocop/strict.yml delete mode 100644 app/frontend/components/drawer/NoUserAppDrawer.tsx delete mode 100644 app/frontend/components/legislator/LegislatorRoute.tsx delete mode 100644 app/frontend/components/shared/FlexRowDiv.tsx delete mode 100644 app/frontend/components/user/settings/UserCongratulationsSettings.tsx delete mode 100644 app/frontend/hooks/useAwardsCount.ts delete mode 100644 app/frontend/hooks/useUserVote.ts delete mode 100644 app/frontend/sway_utils/bills.ts create mode 100644 db/migrate/20240826125722_add_unique_index_by_external_id_to_bills.rb create mode 100644 gemfiles/rubocop.gemfile diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..68a42507 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,21 @@ +#!/usr/bin/env zsh + +set -eu + +changed_files=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') + +echo "Running prettier on changed files" +npx prettier $changed_files --write --ignore-unknown + +echo "Run ESLint on changed files" +npx eslint -c eslint.config.js --no-warn-ignored $changed_files + +echo "Run tsc on project" +npx tsc --project tsconfig.json + +echo "Run rubocop on changed files, autofix any fixable offenses" +bundle exec rubocop --autocorrect --only-recognized-file-types $changed_files + +bundle exec annotate --models + +git update-index --again \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..51ebc2ec --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,22 @@ +inherit_mode: + merge: + - Exclude + +require: + - standard + - rubocop-performance + +inherit_gem: + standard: config/base.yml + standard-performance: config/base.yml + standard-custom: config/base.yml + +inherit_from: + - .rubocop/rails.yml + - .rubocop/rspec.yml + - .rubocop/strict.yml + +AllCops: + NewCops: disable + SuggestExtensions: false + TargetRubyVersion: 3.2 diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml new file mode 100644 index 00000000..fd1fce5e --- /dev/null +++ b/.rubocop/rails.yml @@ -0,0 +1,198 @@ +# Based on removed standard configuration: +# https://github.com/testdouble/standard/commit/94d133f477a5694084ac974d5ee01e8a66ce777e#diff-65478e10d5b2ef41c7293a110c0e6b7c + +require: + - rubocop-rails + +Rails/ActionFilter: + Enabled: true + EnforcedStyle: action + Include: + - app/controllers/**/*.rb + +Rails/ActiveRecordAliases: + Enabled: true + +Rails/ActiveSupportAliases: + Enabled: true + +Rails/ApplicationJob: + Enabled: true + +Rails/ApplicationRecord: + Enabled: true + +Rails/AssertNot: + Enabled: true + Include: + - "**/test/**/*" + +Rails/Blank: + Enabled: true + # Convert usages of `nil? || empty?` to `blank?` + NilOrEmpty: true + # Convert usages of `!present?` to `blank?` + NotPresent: true + # Convert usages of `unless present?` to `if blank?` + UnlessPresent: true + +Rails/BulkChangeTable: + Enabled: true + Database: null + Include: + - db/migrate/*.rb + +Rails/CreateTableWithTimestamps: + Enabled: true + Include: + - db/migrate/*.rb + +Rails/Date: + Enabled: true + EnforcedStyle: flexible + +Rails/Delegate: + Enabled: true + EnforceForPrefixed: true + +Rails/DelegateAllowBlank: + Enabled: true + +Rails/DynamicFindBy: + Enabled: true + Whitelist: + - find_by_sql + +Rails/EnumUniqueness: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/EnvironmentComparison: + Enabled: true + +Rails/Exit: + Enabled: true + Include: + - app/**/*.rb + - config/**/*.rb + - lib/**/*.rb + Exclude: + - lib/**/*.rake + +Rails/FilePath: + Enabled: true + EnforcedStyle: arguments + +Rails/FindBy: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/FindEach: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/HasAndBelongsToMany: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/HttpPositionalArguments: + Enabled: true + Include: + - "spec/**/*" + - "test/**/*" + +Rails/HttpStatus: + Enabled: true + EnforcedStyle: symbolic + +Rails/InverseOf: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/LexicallyScopedActionFilter: + Enabled: true + Safe: false + Include: + - app/controllers/**/*.rb + +Rails/NotNullColumn: + Enabled: true + Include: + - db/migrate/*.rb + +Rails/Output: + Enabled: true + Include: + - app/**/*.rb + - config/**/*.rb + - db/**/*.rb + - lib/**/*.rb + +Rails/OutputSafety: + Enabled: true + +Rails/PluralizationGrammar: + Enabled: true + +Rails/Presence: + Enabled: true + +Rails/Present: + Enabled: true + NotNilAndNotEmpty: true + NotBlank: true + UnlessBlank: true + +Rails/ReadWriteAttribute: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/RedundantReceiverInWithOptions: + Enabled: true + +Rails/RefuteMethods: + Enabled: true + Include: + - "**/test/**/*" + +Rails/RelativeDateConstant: + Enabled: true + AutoCorrect: false + +Rails/RequestReferer: + Enabled: true + EnforcedStyle: referer + +Rails/ReversibleMigration: + Enabled: true + Include: + - db/migrate/*.rb + +Rails/SafeNavigation: + Enabled: true + ConvertTry: false + +Rails/ScopeArgs: + Enabled: true + Include: + - app/models/**/*.rb + +Rails/TimeZone: + Enabled: true + EnforcedStyle: flexible + +Rails/UniqBeforePluck: + Enabled: true + EnforcedStyle: conservative + AutoCorrect: false + +Rails/Validation: + Enabled: true + Include: + - app/models/**/*.rb diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml new file mode 100644 index 00000000..d6d624d2 --- /dev/null +++ b/.rubocop/rspec.yml @@ -0,0 +1,56 @@ +require: + - rubocop-rspec + +# Disable all cops by default, +# only enable those defined explcitly in this configuration file +RSpec: + Enabled: false + +RSpec/Focus: + Enabled: true + +RSpec/EmptyExampleGroup: + Enabled: true + +RSpec/EmptyLineAfterExampleGroup: + Enabled: true + +RSpec/EmptyLineAfterFinalLet: + Enabled: true + +RSpec/EmptyLineAfterHook: + Enabled: true + +RSpec/EmptyLineAfterSubject: + Enabled: true + +RSpec/HookArgument: + Enabled: true + +RSpec/HooksBeforeExamples: + Enabled: true + +RSpec/ImplicitExpect: + Enabled: true + +RSpec/IteratedExpectation: + Enabled: true + +RSpec/LetBeforeExamples: + Enabled: true + +RSpec/MissingExampleGroupArgument: + Enabled: true + +RSpec/ReceiveCounts: + Enabled: true +# The below create errors in ruby lsp + +# Capybara/CurrentPathExpectation: +# Enabled: true + +# RSpec/FactoryBot/AttributeDefinedStatically: +# Enabled: true + +# RSpec/FactoryBot/CreateList: +# Enabled: true diff --git a/.rubocop/strict.yml b/.rubocop/strict.yml new file mode 100644 index 00000000..3d48d19c --- /dev/null +++ b/.rubocop/strict.yml @@ -0,0 +1,19 @@ +Lint/Debugger: # don't leave binding.pry + Enabled: true + Exclude: [] + +RSpec/Focus: # run ALL tests on CI + Enabled: true + Exclude: [] + +Rails/Output: # Don't leave puts-debugging + Enabled: true + Exclude: [] + +Rails/FindEach: # each could badly affect the performance, use find_each + Enabled: true + Exclude: [] + +Rails/UniqBeforePluck: # uniq.pluck and not pluck.uniq + Enabled: true + Exclude: [] diff --git a/Gemfile b/Gemfile index 4e5e470c..f6108af6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,17 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" -ruby '3.3.4' +ruby "3.3.4" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem 'rails', '~> 7.1.3', '>= 7.1.3.2' +gem "rails", "~> 7.1.3", ">= 7.1.3.2" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem 'sprockets-rails' +gem "sprockets-rails" # Use the Puma web server [https://github.com/puma/puma] -gem 'puma', '>= 5.0' +gem "puma", ">= 5.0" # # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] # gem 'importmap-rails' @@ -28,10 +28,10 @@ gem 'puma', '>= 5.0' # Build JSON APIs with ease [https://github.com/rails/jbuilder] # Camel Case json keys # https://stackoverflow.com/questions/23794276/rails-render-json-object-with-camelcase -gem 'jbuilder' +gem "jbuilder" # Use Redis adapter to run Action Cable in production -gem 'redis', '>= 4.0.1' +# gem 'redis', '>= 4.0.1' # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" @@ -40,114 +40,107 @@ gem 'redis', '>= 4.0.1' # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: %i[windows jruby] +gem "tzinfo-data", platforms: %i[windows jruby] # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', require: false +gem "bootsnap", require: false # https://github.com/alexreisner/geocoder?tab=readme-ov-file#testing -gem 'geocoder' +gem "geocoder" # https://rgeo.info/ # https://github.com/rgeo/rgeo -gem 'rgeo' +gem "rgeo" # https://github.com/rgeo/rgeo-geojson -gem 'rgeo-geojson' +gem "rgeo-geojson" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" # https://github.com/BrandonShar/inertia-rails-template/blob/main/Gemfile -gem 'vite_rails' +gem "vite_rails" # https://github.com/inertiajs/inertia-rails?tab=readme-ov-file -gem 'inertia_rails' +gem "inertia_rails" # https://github.com/lostisland/faraday # https://medium.com/@zozulyak.nick/ruby-class-pattern-to-work-with-api-requests-with-built-in-async-approach-bf0713a7dc96 -gem 'concurrent-ruby' -gem 'faraday' +gem "concurrent-ruby" +gem "faraday" # gem 'faraday_curl' # https://github.com/cedarcode/webauthn-ruby -gem 'webauthn' +gem "webauthn" # phone/sms verification # https://www.twilio.com/docs/verify/sms -gem 'twilio-ruby' +gem "twilio-ruby" # Use sqlite3 as the database for Active Record # https://github.com/sparklemotion/sqlite3-ruby/pull/402/files -gem 'sqlite3', '~> 1.7', force_ruby_platform: true +gem "sqlite3", "~> 1.7", force_ruby_platform: true # Ruby type hints # https://sorbet.org/docs/adopting -gem 'sorbet-runtime' +gem "sorbet-runtime" # gcp storage for get/put org icons, etc. -gem 'google-cloud-storage', '~> 1.51' +gem "google-cloud-storage", "~> 1.51" # shorten invite urls # https://github.com/jpmcgrath/shortener -gem 'shortener' +gem "shortener" # Develop? https://github.com/jpmcgrath/shortener/pull/165 # gem 'shortener', :git => "https://github.com/jpmcgrath/shortener.git", :branch => "develop" # https://github.com/pushpad/web-push # https://medium.com/@dejanvu.developer/implementing-web-push-notifications-in-a-ruby-on-rails-application-dcd829e02df0 -gem 'web-push' +gem "web-push" group :production do - gem 'scout_apm' + gem "scout_apm" end group :development, :test do - # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem 'debug', platforms: %i[mri windows] + gem "debug", platforms: %i[mri windows] + + gem "pry" - gem 'pry' + # https://github.com/bkeepers/dotenv + gem "dotenv" # https://github.com/rspec/rspec-rails # Run against this stable release - gem 'rspec-rails', '~> 6' + gem "rspec-rails", "~> 6" # https://github.com/thoughtbot/factory_bot_rails - gem 'factory_bot_rails' + gem "factory_bot_rails" # https://github.com/samuelgiles/rspec-sorbet # https://stackoverflow.com/questions/74842832/how-to-configure-sorbet-with-rspec - gem 'rspec-sorbet' + gem "rspec-sorbet" # Generate types from gems # https://github.com/Shopify/tapioca - gem 'tapioca', require: false + gem "tapioca", require: false end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] - gem 'web-console' - - # https://github.com/kirillplatonov/hotwire-livereload - # https://dev.to/thomasvanholder/how-to-set-up-rails-hotwire-live-reload-38i9 - gem 'hotwire-livereload', '~> 1.3' + gem "web-console" # https://github.com/ctran/annotate_models # https://stackoverflow.com/questions/1289557/how-do-you-discover-model-attributes-in-rails - gem 'annotate' - - # https://github.com/bkeepers/dotenv - gem 'dotenv', groups: %i[development test] - - gem 'rubocop' + gem "annotate" # Ruby type hints # https://sorbet.org/docs/adopting - gem 'sorbet' + gem "sorbet" # https://github.com/faker-ruby/faker - gem 'faker' + gem "faker" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" @@ -158,12 +151,14 @@ group :development do # https://github.com/BetterErrors/better_errors gem "better_errors" gem "binding_of_caller" + + eval_gemfile "gemfiles/rubocop.gemfile" end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] - gem 'capybara' - gem 'selenium-webdriver' + gem "capybara" + gem "selenium-webdriver" - gem 'rails-controller-testing' + gem "rails-controller-testing" end diff --git a/Gemfile.lock b/Gemfile.lock index d3b44f05..53451ab9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -139,12 +139,6 @@ GEM logger faraday-net_http (3.1.1) net-http - ffi (1.17.0-aarch64-linux-gnu) - ffi (1.17.0-aarch64-linux-musl) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) - ffi (1.17.0-x86_64-linux-musl) geocoder (1.8.3) base64 (>= 0.1.0) csv (>= 3.0.0) @@ -184,10 +178,6 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hotwire-livereload (1.4.0) - actioncable (>= 6.0.0) - listen (>= 3.0.0) - railties (>= 6.0.0) httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) @@ -204,9 +194,7 @@ GEM jwt (2.8.2) base64 language_server-protocol (3.17.0.3) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) + lint_roller (1.1.0) logger (1.6.0) loofah (2.22.0) crass (~> 1.0.2) @@ -309,18 +297,11 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) rbi (0.1.14) prism (>= 0.18.0, < 1.0.0) sorbet-runtime (>= 0.5.9204) rdoc (6.7.0) psych (>= 4.0.0) - redis (5.3.0) - redis-client (>= 0.22.0) - redis-client (0.22.2) - connection_pool regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) @@ -368,6 +349,22 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.32.1) parser (>= 3.3.1.0) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-performance (1.21.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.25.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rspec (3.0.4) + rubocop (~> 1.61) + rubocop-shopify (2.15.1) + rubocop (~> 1.51) + rubocop-thread_safety (0.5.1) + rubocop (>= 0.90.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) safety_net_attestation (0.4.0) @@ -410,6 +407,18 @@ GEM sprockets (>= 3.0.0) sqlite3 (1.7.3) mini_portile2 (~> 2.8.0) + standard (1.40.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.65.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.4) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.4.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.21.0) stringio (3.1.1) strscan (3.1.0) tapioca (0.16.1) @@ -501,19 +510,23 @@ DEPENDENCIES faraday geocoder google-cloud-storage (~> 1.51) - hotwire-livereload (~> 1.3) inertia_rails jbuilder pry puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.2) rails-controller-testing - redis (>= 4.0.1) rgeo rgeo-geojson rspec-rails (~> 6) rspec-sorbet - rubocop + rubocop! + rubocop-factory_bot! + rubocop-performance! + rubocop-rails! + rubocop-rspec! + rubocop-shopify! + rubocop-thread_safety! scout_apm selenium-webdriver shortener @@ -521,6 +534,7 @@ DEPENDENCIES sorbet-runtime sprockets-rails sqlite3 (~> 1.7) + standard (~> 1.35)! tapioca twilio-ruby tzinfo-data diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index db9b55f3..261e32b0 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true # typed: strict + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 707be803..4804259d 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true # typed: strict + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/controllers/admin/bills/creator_controller.rb b/app/controllers/admin/bills/creator_controller.rb index 28e94def..1d390ed5 100644 --- a/app/controllers/admin/bills/creator_controller.rb +++ b/app/controllers/admin/bills/creator_controller.rb @@ -1,7 +1,13 @@ -class Admin::Bills::CreatorController < ApplicationController - before_action :verify_is_admin +# frozen_string_literal: true - def index - render inertia: "BillOfTheWeekCreator" +module Admin + module Bills + class CreatorController < ApplicationController + before_action :verify_is_admin + + def index + render inertia: "BillOfTheWeekCreator" + end + end end end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 08172334..8f732811 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AdminController < ApplicationController before_action :verify_is_admin end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3a47b34c..7787ddfa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class ApplicationController < ActionController::Base @@ -14,33 +15,33 @@ class ApplicationController < ActionController::Base helper_method :current_user, :current_sway_locale, :verify_is_admin - @@SSRMethods = {} + @@_ssr_methods = {} ROUTES = T.let({ - HOME: 'home', - LEGISLATORS: 'legislators', - REGISTRATION: 'sway_registration', - BILL_OF_THE_WEEK: 'bill_of_the_week', - BILL: 'bill', - BILLS: 'bills', - BILL_CREATOR: 'admin/bills/creator', - INFLUENCE: 'influence', - INVITE: 'invites/:user_id/:invite_uuid', - NOTIFICATIONS: 'notifications' - }, T::Hash[Symbol, T.nilable(String)]) + HOME: "home", + LEGISLATORS: "legislators", + REGISTRATION: "sway_registration", + BILL_OF_THE_WEEK: "bill_of_the_week", + BILL: "bill", + BILLS: "bills", + BILL_CREATOR: "admin/bills/creator", + INFLUENCE: "influence", + INVITE: "invites/:user_id/:invite_uuid", + NOTIFICATIONS: "notifications" + }, T::Hash[Symbol, T.nilable(String)]) PAGES = T.let({ - HOME: 'Home', - LEGISLATORS: 'Legislators', - REGISTRATION: 'Registration', - BILL: 'Bill', - BILLS: 'Bills', - BILL_OF_THE_WEEK: 'BillOfTheWeek', - BILL_CREATOR: 'BillOfTheWeekCreator', - INFLUENCE: 'Influence', - INVITE: 'Invite', - NOTIFICATIONS: 'Notifications' - }, T::Hash[Symbol, T.nilable(String)]) + HOME: "Home", + LEGISLATORS: "Legislators", + REGISTRATION: "Registration", + BILL: "Bill", + BILLS: "Bills", + BILL_OF_THE_WEEK: "BillOfTheWeek", + BILL_CREATOR: "BillOfTheWeekCreator", + INFLUENCE: "Influence", + INVITE: "Invite", + NOTIFICATIONS: "Notifications" + }, T::Hash[Symbol, T.nilable(String)]) sig do params( @@ -65,11 +66,11 @@ def render_component(page, props = {}) else # redirect_to legislator_path render inertia: page, - props: { - user: u.to_builder.attributes!, - swayLocale: current_sway_locale&.to_builder(current_user)&.attributes!, - **expand_props(props) - } + props: { + user: u.to_builder.attributes!, + swayLocale: current_sway_locale&.to_builder(current_user)&.attributes!, + **expand_props(props) + } end end @@ -79,7 +80,7 @@ def render_component(page, props = {}) props: T.untyped ).returns(T.untyped) end - def redirect_component(page, props = {}) + def redirect_component(page, _props = {}) redirect_to root_path if page.nil? u = current_user @@ -100,12 +101,12 @@ def route_component(route) u = current_user if u.nil? - render json: { route: ROUTES[:HOME] } + render json: {route: ROUTES[:HOME]} elsif !u.is_registration_complete - render json: { route: ROUTES[:REGISTRATION], phone: } + render json: {route: ROUTES[:REGISTRATION], phone:} else Rails.logger.info "ServerRendering.route - Route to page - #{route}" - render json: { route:, phone: } + render json: {route:, phone:} end end @@ -115,10 +116,10 @@ def route_component(route) end def method_missing(method_name, *args) mn = method_name.to_s - if mn.start_with?('render_') - @@SSRMethods[method_name] = lambda do + if mn.start_with?("render_") + @@_ssr_methods[method_name] = lambda do Rails.logger.info "SSR RENDERING - #{mn}" - page = PAGES.dig(T.cast(mn.split('_')[1..]&.map(&:upcase)&.join('_')&.to_sym, Symbol)) + page = PAGES[T.cast(mn.split("_")[1..]&.map(&:upcase)&.join("_")&.to_sym, Symbol)] callable = T.cast(args.first, T.nilable(T.any(T::Hash[T.untyped, T.untyped], T.proc.returns(T::Hash[T.untyped, T.untyped])))) if callable.nil? @@ -127,27 +128,27 @@ def method_missing(method_name, *args) render_component(page, callable) end end - @@SSRMethods[method_name].call - elsif mn.start_with?('route_') - @@SSRMethods[method_name] = lambda do + @@_ssr_methods[method_name].call + elsif mn.start_with?("route_") + @@_ssr_methods[method_name] = lambda do Rails.logger.info "SSR ROUTING TO - #{mn}" - route_component(ROUTES.dig(T.cast(mn.split('_')[1..]&.map(&:upcase)&.join('_')&.to_sym, Symbol))) + route_component(ROUTES[T.cast(mn.split("_")[1..]&.map(&:upcase)&.join("_")&.to_sym, Symbol)]) end - @@SSRMethods[method_name].call - elsif mn.start_with?('redirect_') - @@SSRMethods[method_name] = lambda do + @@_ssr_methods[method_name].call + elsif mn.start_with?("redirect_") + @@_ssr_methods[method_name] = lambda do Rails.logger.info "SSR REDIRECTING TO - #{mn}" - redirect_component("#{mn.split('_')[1..]&.join('_')}_path") + redirect_component("#{mn.split("_")[1..]&.join("_")}_path") end - @@SSRMethods[method_name].call + @@_ssr_methods[method_name].call else raise NoMethodError end end # https://www.leighhalliday.com/ruby-metaprogramming-method-missing - def respond_to?(method_name, include_private = false) - @@SSRMethods.include?(method_name.to_sym) || super + def respond_to_missing?(method_name, include_private = false) + @@_ssr_methods.include?(method_name.to_sym) || super end sig { void } @@ -159,7 +160,7 @@ def test_recaptcha sig { params(user: T.nilable(User)).returns(T.untyped) } def sign_in(user) - return unless user.present? + return if user.blank? invited_by_id = session[UserInviter::INVITED_BY_SESSION_KEY] @@ -213,14 +214,14 @@ def current_sway_locale def redirect_if_no_current_user u = current_user if u.nil? - Rails.logger.info 'No current user, redirect to root path' + Rails.logger.info "No current user, redirect to root path" redirect_to root_path elsif !u.is_registration_complete if u.has_user_legislators? u.is_registration_complete = true u.save! else - Rails.logger.info 'Current user registration is not complete, redirect to sway registration' + Rails.logger.info "Current user registration is not complete, redirect to sway registration" redirect_to sway_registration_index_path end end @@ -232,8 +233,8 @@ def verify_is_admin end def set_sway_locale_id_in_session - if params[:sway_locale_id].present? - session[:sway_locale_id] = params[:sway_locale_id].to_i - end + return if params[:sway_locale_id].blank? + + session[:sway_locale_id] = params[:sway_locale_id].to_i end end diff --git a/app/controllers/bill_of_the_week_controller.rb b/app/controllers/bill_of_the_week_controller.rb index 32edf94e..55de74b3 100644 --- a/app/controllers/bill_of_the_week_controller.rb +++ b/app/controllers/bill_of_the_week_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class BillOfTheWeekController < ApplicationController diff --git a/app/controllers/bill_score_districts_controller.rb b/app/controllers/bill_score_districts_controller.rb index 5e4dacbf..19190753 100644 --- a/app/controllers/bill_score_districts_controller.rb +++ b/app/controllers/bill_score_districts_controller.rb @@ -1,14 +1,13 @@ +# frozen_string_literal: true # typed: true class BillScoreDistrictsController < ApplicationController - - # GET /bill_score_districts/1 or /bill_score_districts/1.json def show render json: BillScoreDistrict.where(bill_score_id: bill_score_district_params[:bill_score_id]).map { |bsd| bsd.to_builder.attributes! }, - status: :ok + status: :ok end private diff --git a/app/controllers/bill_scores_controller.rb b/app/controllers/bill_scores_controller.rb index a30b51f2..555ca1c3 100644 --- a/app/controllers/bill_scores_controller.rb +++ b/app/controllers/bill_scores_controller.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true # typed: true class BillScoresController < ApplicationController - - # GET /bill_scores/1 or /bill_scores/1.json def show render json: BillScore.find_by(bill_id: params[:id])&.to_builder&.target!, status: :ok diff --git a/app/controllers/bills_controller.rb b/app/controllers/bills_controller.rb index e1592b5c..d7001bad 100644 --- a/app/controllers/bills_controller.rb +++ b/app/controllers/bills_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class BillsController < ApplicationController @@ -29,31 +30,31 @@ def show # GET /bills/new def new T.unsafe(self).render_bill_creator({ - bills: (current_sway_locale&.bills || []).map { |b| b.to_builder.attributes! }, - bill: Bill.new.attributes, - legislators: (current_sway_locale&.legislators || []).map do |l| - l.to_builder.attributes! - end, - legislatorVotes: [], - positions: [] - }) + bills: (current_sway_locale&.bills || []).map { |b| b.to_builder.attributes! }, + bill: Bill.new.attributes, + legislators: (current_sway_locale&.legislators || []).map do |l| + l.to_builder.attributes! + end, + legislatorVotes: [], + positions: [] + }) end # GET /bills/1/edit def edit - return unless @bill.present? + return if @bill.blank? T.unsafe(self).render_bill_creator({ - bills: (current_sway_locale&.bills || []).map { |b| b.to_builder.attributes! }, - bill: @bill.to_builder.attributes!, - legislators: (current_sway_locale&.legislators || []).map do |l| - l.to_builder.attributes! - end, - legislatorVotes: @bill.legislator_votes.map { |lv| lv.to_builder.attributes! }, - positions: @bill.organization_bill_positions.map do |obp| - obp.to_builder.attributes! - end - }) + bills: (current_sway_locale&.bills || []).map { |b| b.to_builder.attributes! }, + bill: @bill.to_builder.attributes!, + legislators: (current_sway_locale&.legislators || []).map do |l| + l.to_builder.attributes! + end, + legislatorVotes: @bill.legislator_votes.map { |lv| lv.to_builder.attributes! }, + positions: @bill.organization_bill_positions.map do |obp| + obp.to_builder.attributes! + end + }) end # POST /bills or /bills.json @@ -70,7 +71,7 @@ def create # PATCH/PUT /bills/1 or /bills/1.json def update - render json: { success: false, message: @bill.errors.join(', ') }, status: :ok unless @bill.present? + render json: {success: false, message: @bill.errors.join(", ")}, status: :ok if @bill.blank? current_audio_path = @bill.audio_bucket_path.freeze if @bill.update(bill_params) @@ -80,7 +81,7 @@ def update render json: @bill.to_builder.attributes!, status: :ok else - render json: { success: false, message: @bill.errors.join(', ') }, status: :ok + render json: {success: false, message: @bill.errors.join(", ")}, status: :ok end end @@ -111,8 +112,8 @@ def create_vote(b) return unless vote_params[:house_roll_call_vote_number] || vote_params[:senate_roll_call_vote_number] Vote.find_or_create_by!(bill_id: b.id, - house_roll_call_vote_number: vote_params[:house_roll_call_vote_number], - senate_roll_call_vote_number: vote_params[:senate_roll_call_vote_number]) + house_roll_call_vote_number: vote_params[:house_roll_call_vote_number], + senate_roll_call_vote_number: vote_params[:senate_roll_call_vote_number]) end def vote_params diff --git a/app/controllers/buckets/assets_controller.rb b/app/controllers/buckets/assets_controller.rb index 3b06c6a0..35430d18 100644 --- a/app/controllers/buckets/assets_controller.rb +++ b/app/controllers/buckets/assets_controller.rb @@ -1,34 +1,38 @@ +# frozen_string_literal: true # typed: true -class Buckets::AssetsController < ApplicationController - extend T::Sig - include SwayGoogleCloudStorage +module Buckets + class AssetsController < ApplicationController + extend T::Sig + include SwayGoogleCloudStorage - before_action :verify_is_admin, only: %i[create] + before_action :verify_is_admin, only: %i[create] - # Upload a file to the assets bucket in GCP - # return the new file location - def create - render json: { - bucketFilePath: file_name, - url: generate_put_signed_url_v4(bucket_name: SwayGoogleCloudStorage::BUCKETS[:ASSETS], file_name:, content_type: buckets_assets_params[:mime_type]) - }, status: :ok - end + # Upload a file to the assets bucket in GCP + # return the new file location + def create + render json: { + bucketFilePath: file_name, + url: generate_put_signed_url_v4(bucket_name: SwayGoogleCloudStorage::BUCKETS[:ASSETS], file_name:, + content_type: buckets_assets_params[:mime_type]) + }, status: :ok + end - private + private - def file_name - "#{T.cast(current_sway_locale, SwayLocale).name}/#{buckets_assets_params[:name]}" - end + def file_name + "#{T.cast(current_sway_locale, SwayLocale).name}/#{buckets_assets_params[:name]}" + end - # https://stackoverflow.com/a/16804560/6410635 - def file_suffix - Rack::Mime::MIME_TYPES.invert[buckets_assets_params[:mime_type]] - end + # https://stackoverflow.com/a/16804560/6410635 + def file_suffix + Rack::Mime::MIME_TYPES.invert[buckets_assets_params[:mime_type]] + end - # :name will typically be the name of the organization uploading an icon - # :mime_type is the type of file being uploaded, i.e. image/png - def buckets_assets_params - params.require(:asset).permit(:name, :mime_type) + # :name will typically be the name of the organization uploading an icon + # :mime_type is the type of file being uploaded, i.e. image/png + def buckets_assets_params + params.require(:asset).permit(:name, :mime_type) + end end end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 957a1f28..b5a908da 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true module Authentication @@ -42,7 +43,7 @@ def send_phone_verification(session, phone_) begin verification = twilio_client.verify.v2.services(service_sid).verifications.create( to: "+1#{phone}", - channel: 'sms' + channel: "sms" ) session[:phone] = phone if verification.present? @@ -57,19 +58,19 @@ def send_phone_verification(session, phone_) private def twilio_client - @client ||= Twilio::REST::Client.new(account_sid, auth_token) + @twilio_client ||= Twilio::REST::Client.new(account_sid, auth_token) end def account_sid - ENV['TWILIO_ACCOUNT_SID'] + ENV["TWILIO_ACCOUNT_SID"] end def auth_token - ENV['TWILIO_AUTH_TOKEN'] + ENV["TWILIO_AUTH_TOKEN"] end def service_sid - ENV['TWILIO_VERIFY_SERVICE_SID'] + ENV["TWILIO_VERIFY_SERVICE_SID"] end # sig { params(phone: String).returns(T::Boolean) } @@ -89,7 +90,7 @@ def passkey? sig { params(phone: String).returns(T.nilable(User)) } def find_by_phone(phone) - @user ||= User.find_by(phone:) + @find_by_phone ||= User.find_by(phone:) end end end diff --git a/app/controllers/concerns/relying_party.rb b/app/controllers/concerns/relying_party.rb index d89c3989..b583b17d 100644 --- a/app/controllers/concerns/relying_party.rb +++ b/app/controllers/concerns/relying_party.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true module RelyingParty @@ -10,10 +11,10 @@ def relying_party WebAuthn::RelyingParty.new( # This value needs to match `window.location.origin` evaluated by # the User Agent during registration and authentication ceremonies. - origin: Rails.env.production? ? 'https://app.sway.vote' : 'https://localhost:3000', + origin: Rails.env.production? ? "https://app.sway.vote" : "https://localhost:3000", # Relying Party name for display purposes - name: "sway-#{ENV['RAILS_ENV']}" + name: "sway-#{ENV["RAILS_ENV"]}" # Optionally configure a client timeout hint, in milliseconds. # This hint specifies how long the browser should wait for any # interaction with the user. diff --git a/app/controllers/concerns/sway_props.rb b/app/controllers/concerns/sway_props.rb index 32ad13f3..05f17a54 100644 --- a/app/controllers/concerns/sway_props.rb +++ b/app/controllers/concerns/sway_props.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true module SwayProps @@ -8,9 +9,9 @@ module SwayProps sig do params( props: T.nilable(T.any( - T::Hash[T.untyped, T.untyped], - T.proc.returns(T::Hash[T.untyped, T.untyped]) - )) + T::Hash[T.untyped, T.untyped], + T.proc.returns(T::Hash[T.untyped, T.untyped]) + )) ).returns(T::Hash[T.untyped, T.untyped]) end def expand_props(props) diff --git a/app/controllers/districts_controller.rb b/app/controllers/districts_controller.rb index c58fed1f..63a7ff10 100644 --- a/app/controllers/districts_controller.rb +++ b/app/controllers/districts_controller.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true # typed: true -class DistrictsController < ApplicationController +class DistrictsController < ApplicationController def index render json: current_user&.districts(T.cast(current_sway_locale, SwayLocale)), status: :ok end - end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 24780a0c..cc61ef5b 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,7 +7,7 @@ class HomeController < ApplicationController def index u = current_user if u.nil? - T.unsafe(self).render_home({name: 'Sway', isBubbles: true}) + T.unsafe(self).render_home({name: "Sway", isBubbles: true}) elsif u.is_registration_complete redirect_to legislators_path else diff --git a/app/controllers/influence_controller.rb b/app/controllers/influence_controller.rb index a1146be0..fc19fcc6 100644 --- a/app/controllers/influence_controller.rb +++ b/app/controllers/influence_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class InfluenceController < ApplicationController @@ -9,10 +10,10 @@ def index if u.nil? || l.nil? redirect_to root_path elsif u.is_registration_complete - T.unsafe(self).render_influence({ influence: InfluenceService.new( + T.unsafe(self).render_influence({influence: InfluenceService.new( user: u, sway_locale: l - ).to_builder.attributes! }) + ).to_builder.attributes!}) else redirect_to sway_registration_index_path end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index b77b5636..05a469aa 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class InvitesController < ApplicationController diff --git a/app/controllers/legislator_votes_controller.rb b/app/controllers/legislator_votes_controller.rb index be64ce06..fbf323b3 100644 --- a/app/controllers/legislator_votes_controller.rb +++ b/app/controllers/legislator_votes_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class LegislatorVotesController < ApplicationController @@ -16,7 +17,7 @@ def show def create LegislatorVote.where(bill_id: legislator_votes_params[:votes].first[:bill_id]).destroy_all - render json: LegislatorVote.insert_all!(legislator_votes_params[:votes]), status: :ok + render json: LegislatorVote.insert_all!(legislator_votes_params[:votes]), status: :ok # rubocop:disable Rails/SkipsModelValidations end def update diff --git a/app/controllers/legislators_controller.rb b/app/controllers/legislators_controller.rb index ea43566d..ed8c7353 100644 --- a/app/controllers/legislators_controller.rb +++ b/app/controllers/legislators_controller.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true # typed: true class LegislatorsController < ApplicationController - - # GET /legislators or /legislators.json def index T.unsafe(self).render_legislators( diff --git a/app/controllers/notifications/push_notification_subscriptions_controller.rb b/app/controllers/notifications/push_notification_subscriptions_controller.rb index 7c18311e..8b4ea847 100644 --- a/app/controllers/notifications/push_notification_subscriptions_controller.rb +++ b/app/controllers/notifications/push_notification_subscriptions_controller.rb @@ -1,47 +1,52 @@ +# frozen_string_literal: true # typed: true -class Notifications::PushNotificationSubscriptionsController < ApplicationController - extend T::Sig - - before_action :set_subscription - - def create - if @subscription.present? - @subscription.update!(subscribed: true) unless @subscription.subscribed +module Notifications + class PushNotificationSubscriptionsController < ApplicationController + extend T::Sig + + before_action :set_subscription + + def create + if @subscription.present? + @subscription.update!(subscribed: true) unless @subscription.subscribed + + SwayPushNotificationService.new( + title: "Notifications Activated", + body: "We'll send you one of these when a new Bill of the Week is released." + ).send_push_notification + + render json: @subscription.attributes, status: :ok + else + render json: PushNotificationSubscription.create!( + **push_notification_subscription_params, + user: current_user, + subscribed: true + ).attributes, status: :ok + end + end - SwayPushNotificationService.new( - title: 'Notifications Activated', - body: "We'll send you one of these when a new Bill of the Week is released." - ).send_push_notification + def destroy + return if @subscription.blank? + @subscription.update!(subscribed: false) render json: @subscription.attributes, status: :ok - else - render json: PushNotificationSubscription.create!( - **push_notification_subscription_params, - user: current_user, - subscribed: true - ).attributes, status: :ok end - end - - def destroy - return unless @subscription.present? - @subscription.update!(subscribed: false) - render json: @subscription.attributes, status: :ok - end - - private + private - def set_subscription - @subscription = current_user&.push_notification_subscriptions&.find { |s| s.endpoint == push_notification_subscription_params[:endpoint] } - end + def set_subscription + @subscription = current_user&.push_notification_subscriptions&.find do |s| + s.endpoint == push_notification_subscription_params[:endpoint] + end + end - def push_notification_subscription_params - params.require(:push_notification_subscription).permit( - :endpoint, - :p256dh, - :auth - ) + def push_notification_subscription_params + params.require(:push_notification_subscription).permit( + :endpoint, + :p256dh, + :auth + ) + end end end diff --git a/app/controllers/notifications/push_notifications_controller.rb b/app/controllers/notifications/push_notifications_controller.rb index db2a6cbc..b5a52cc9 100644 --- a/app/controllers/notifications/push_notifications_controller.rb +++ b/app/controllers/notifications/push_notifications_controller.rb @@ -1,31 +1,34 @@ +# frozen_string_literal: true # typed: true -class Notifications::PushNotificationsController < ApplicationController - extend T::Sig +module Notifications + class PushNotificationsController < ApplicationController + extend T::Sig - before_action :set_subscription + before_action :set_subscription - def create - SwayPushNotificationService.new( - @subscription, - title: 'Notifications Test', - body: 'Test Web Push Notification.' - ).send_push_notification + def create + SwayPushNotificationService.new( + @subscription, + title: "Notifications Test", + body: "Test Web Push Notification." + ).send_push_notification - render json: { success: true, message: 'Push Notification Sent' }, status: :ok - end + render json: {success: true, message: "Push Notification Sent"}, status: :ok + end - private + private - def set_subscription - @subscription = current_user&.push_notification_subscriptions&.find do |s| - s.endpoint == push_notification_subscription_params[:endpoint] + def set_subscription + @subscription = current_user&.push_notification_subscriptions&.find do |s| + s.endpoint == push_notification_subscription_params[:endpoint] + end end - end - def push_notification_subscription_params - params.require(:push_notification).permit( - :endpoint - ) + def push_notification_subscription_params + params.require(:push_notification).permit( + :endpoint + ) + end end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 67cf1896..e96da962 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + class NotificationsController < ApplicationController def index T.unsafe(self).render_notifications(lambda do { - subscriptions: (current_user&.push_notification_subscriptions&.map { |s| s.attributes }) || [] + subscriptions: current_user&.push_notification_subscriptions&.map(&:attributes) || [] } end) end diff --git a/app/controllers/organization_bill_positions_controller.rb b/app/controllers/organization_bill_positions_controller.rb index 505ea33d..e15e9b63 100644 --- a/app/controllers/organization_bill_positions_controller.rb +++ b/app/controllers/organization_bill_positions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OrganizationBillPositionsController < ApplicationController before_action :verify_is_admin, only: %i[create] @@ -6,7 +8,7 @@ def index render json: OrganizationBillPosition.where(bill_id: params[:bill_id]), status: :ok elsif params[:organization_id] render json: OrganizationBillPosition.where(organization_id: params[:organization_id]), - status: :ok + status: :ok else render json: [], status: :ok end @@ -23,7 +25,7 @@ def create if organization_bill_positions_params[:positions].present? OrganizationBillPosition.where(bill_id: organization_bill_positions_params[:positions].first[:bill_id]).destroy_all - render json: OrganizationBillPosition.insert_all!(organization_bill_positions_params[:positions]), status: :ok + render json: OrganizationBillPosition.insert_all!(organization_bill_positions_params[:positions]), status: :ok # rubocop:disable Rails/SkipsModelValidations else render json: [], status: :no_content end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 68ca7571..702546e5 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class OrganizationsController < ApplicationController @@ -16,13 +17,13 @@ def show if @organization.present? render json: @organization.to_builder(with_positions: true).attributes!, status: :ok else - render json: { success: false, message: 'Organization not found.' }, status: :ok + render json: {success: false, message: "Organization not found."}, status: :ok end end def create render json: Organization.find_or_create_by!(**organization_params, sway_locale: current_sway_locale).to_builder(with_positions: false).attributes!, - status: :ok + status: :ok end def update @@ -33,7 +34,7 @@ def update render json: @organization.to_builder(with_positions: false).attributes!, status: :ok else - render json: { success: false, message: 'Organization not found.' }, status: :ok + render json: {success: false, message: "Organization not found."}, status: :ok end end @@ -47,9 +48,9 @@ def organization_params params.require(:organization).permit(:name, :icon_path, :sway_locale_id) end - def remove_icon current_icon_path - if @organization.icon_path != current_icon_path - delete_file(bucket_name: SwayGoogleCloudStorage::BUCKETS[:ASSETS], file_name: current_icon_path) - end + def remove_icon(current_icon_path) + return unless @organization.icon_path != current_icon_path + + delete_file(bucket_name: SwayGoogleCloudStorage::BUCKETS[:ASSETS], file_name: current_icon_path) end end diff --git a/app/controllers/phone_verification_controller.rb b/app/controllers/phone_verification_controller.rb index 378ccbed..d2bc9a20 100644 --- a/app/controllers/phone_verification_controller.rb +++ b/app/controllers/phone_verification_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true # https://www.twilio.com/docs/verify/sms @@ -11,22 +12,22 @@ class PhoneVerificationController < ApplicationController def create if Rails.env.production? - render json: { success: send_phone_verification(session, phone_verification_params[:phone]) }, status: :ok + render json: {success: send_phone_verification(session, phone_verification_params[:phone])}, status: :ok else session[:phone] = phone_verification_params[:phone] - render json: { success: true }, status: :ok + render json: {success: true}, status: :ok end end def update if Rails.env.production? verification_check = @client.verify - .v2 - .services(service_sid) - .verification_checks - .create(to: "+1#{session[:phone]}", code: phone_verification_params[:code]) + .v2 + .services(service_sid) + .verification_checks + .create(to: "+1#{session[:phone]}", code: phone_verification_params[:code]) - approved = verification_check&.status == 'approved' + approved = verification_check&.status == "approved" if approved # Do NOT create a user here @@ -40,25 +41,25 @@ def update approved = true end - render json: { success: approved }, status: :ok + render json: {success: approved}, status: :ok end private def set_twilio_client - @client ||= Twilio::REST::Client.new(account_sid, auth_token) + @set_twilio_client ||= Twilio::REST::Client.new(account_sid, auth_token) end def account_sid - ENV['TWILIO_ACCOUNT_SID'] + ENV["TWILIO_ACCOUNT_SID"] end def auth_token - ENV['TWILIO_AUTH_TOKEN'] + ENV["TWILIO_AUTH_TOKEN"] end def service_sid - ENV['TWILIO_VERIFY_SERVICE_SID'] + ENV["TWILIO_VERIFY_SERVICE_SID"] end sig { returns(ActionController::Parameters) } diff --git a/app/controllers/sway_locales_controller.rb b/app/controllers/sway_locales_controller.rb index 19df6bf6..b47925d2 100644 --- a/app/controllers/sway_locales_controller.rb +++ b/app/controllers/sway_locales_controller.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true # typed: true class SwayLocalesController < ApplicationController - - # GET /sway_locales or /sway_locales.json def index render json: current_user&.sway_locales&.map { |s| s.to_builder(current_user).attributes! }, status: :ok diff --git a/app/controllers/sway_registration_controller.rb b/app/controllers/sway_registration_controller.rb index 98cd15d3..5b84c0f3 100644 --- a/app/controllers/sway_registration_controller.rb +++ b/app/controllers/sway_registration_controller.rb @@ -72,6 +72,6 @@ def address sig { returns(ActionController::Parameters) } def sway_registration_params params.require(:sway_registration).permit(:latitude, :longitude, :street, :city, :region, :regionCode, - :postalCode, :country) + :postalCode, :country) end end diff --git a/app/controllers/user_districts_controller.rb b/app/controllers/user_districts_controller.rb index e1fe48db..42d16a8e 100644 --- a/app/controllers/user_districts_controller.rb +++ b/app/controllers/user_districts_controller.rb @@ -1,9 +1,8 @@ +# frozen_string_literal: true # typed: true -class UserDistrictsController < ApplicationController +class UserDistrictsController < ApplicationController def index render json: current_user&.districts(T.cast(current_sway_locale, SwayLocale)), status: :ok end - - end diff --git a/app/controllers/user_legislator_scores_controller.rb b/app/controllers/user_legislator_scores_controller.rb index bd09052c..76b8102f 100644 --- a/app/controllers/user_legislator_scores_controller.rb +++ b/app/controllers/user_legislator_scores_controller.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true # typed: true class UserLegislatorScoresController < ApplicationController - - # GET /user_legislator_scores or /user_legislator_scores.json def index render json: UserLegislatorScore.where(user: current_user).map { |uls| uls.to_builder.attributes! }, status: :ok @@ -14,7 +13,7 @@ def show user: current_user, legislator_id: params[:id] )&.user_legislator_score&.to_builder&.attributes!, - status: :ok + status: :ok end private diff --git a/app/controllers/user_legislators_controller.rb b/app/controllers/user_legislators_controller.rb index b0c022da..e4e5a828 100644 --- a/app/controllers/user_legislators_controller.rb +++ b/app/controllers/user_legislators_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # typed: true class UserLegislatorsController < ApplicationController @@ -23,6 +24,6 @@ def create private def set_sway_locale - @sway_locale ||= SwayLocale.find(params[:sway_locale_id]) + @set_sway_locale ||= SwayLocale.find(params[:sway_locale_id]) end end diff --git a/app/controllers/users/webauthn/registration_controller.rb b/app/controllers/users/webauthn/registration_controller.rb index 9d76d05f..d3943b9e 100644 --- a/app/controllers/users/webauthn/registration_controller.rb +++ b/app/controllers/users/webauthn/registration_controller.rb @@ -2,95 +2,99 @@ # frozen_string_literal: true -class Users::Webauthn::RegistrationController < ApplicationController - extend T::Sig - - before_action :test_recaptcha, only: [:create] - skip_before_action :redirect_if_no_current_user - - def create - user = User.find_or_initialize_by( - phone: session[:verified_phone] - ) - - user.is_phone_verified = session[:verified_phone] == session[:phone] - - if user.is_phone_verified - create_options = relying_party.options_for_registration( - user: { - name: session[:verified_phone], - id: user.webauthn_id - }, - authenticator_selection: { user_verification: 'required' } - ) - - if user.valid? - session[:current_registration] = { challenge: create_options.challenge, user_attributes: user.attributes } - - render json: create_options - else - render json: { errors: user.errors.full_messages }, status: :unprocessable_entity +module Users + module Webauthn + class RegistrationController < ApplicationController + extend T::Sig + + before_action :test_recaptcha, only: [:create] + skip_before_action :redirect_if_no_current_user + + def create + user = User.find_or_initialize_by( + phone: session[:verified_phone] + ) + + user.is_phone_verified = session[:verified_phone] == session[:phone] + + if user.is_phone_verified + create_options = relying_party.options_for_registration( + user: { + name: session[:verified_phone], + id: user.webauthn_id + }, + authenticator_selection: {user_verification: "required"} + ) + + if user.valid? + session[:current_registration] = {challenge: create_options.challenge, user_attributes: user.attributes} + + render json: create_options + else + render json: {errors: user.errors.full_messages}, status: :unprocessable_entity + end + else + render json: {success: false, message: "Please confirm your phone number first."}, status: :ok + end end - else - render json: { success: false, message: 'Please confirm your phone number first.' }, status: :ok - end - end - - def callback - user = User.find_by(phone: session[:verified_phone]) - if user.present? - user.update!(user_attributes) - else - user = User.create!(user_attributes) - end - begin - webauthn_passkey = relying_party.verify_registration( - params, - challenge, - user_verification: true - ) - - passkey = Passkey.new( - user:, - external_id: Base64.strict_encode64(webauthn_passkey.raw_id), - label: params[:passkey_label], - public_key: webauthn_passkey.public_key, - sign_count: webauthn_passkey.sign_count - ) - - if passkey.save - sign_in(user) - - T.unsafe(self).route_registration - else - render json: { - success: false, - message: "Couldn't register your Passkey" - }, status: :unprocessable_entity + def callback + user = User.find_by(phone: session[:verified_phone]) + if user.present? + user.update!(user_attributes) + else + user = User.create!(user_attributes) + end + + begin + webauthn_passkey = relying_party.verify_registration( + params, + challenge, + user_verification: true + ) + + passkey = Passkey.new( + user:, + external_id: Base64.strict_encode64(webauthn_passkey.raw_id), + label: params[:passkey_label], + public_key: webauthn_passkey.public_key, + sign_count: webauthn_passkey.sign_count + ) + + if passkey.save + sign_in(user) + + T.unsafe(self).route_registration + else + render json: { + success: false, + message: "Couldn't register your Passkey" + }, status: :unprocessable_entity + end + rescue WebAuthn::Error => e + render json: { + success: false, + message: "Verification failed: #{e.message}" + }, status: :unprocessable_entity + ensure + session.delete(:current_registration) + end end - rescue WebAuthn::Error => e - render json: { - success: false, - message: "Verification failed: #{e.message}" - }, status: :unprocessable_entity - ensure - session.delete(:current_registration) - end - end - def user_attributes - session.dig(:current_registration, 'user_attributes') - end + def user_attributes + session.dig(:current_registration, "user_attributes") + end - def challenge - session.dig(:current_registration, 'challenge') - end + def challenge + session.dig(:current_registration, "challenge") + end - private + private - sig { returns(ActionController::Parameters) } - def registration_params - params.require(:registration).permit(:passkey_label, :token) + sig { returns(ActionController::Parameters) } + def registration_params + params.require(:registration).permit(:passkey_label, :token) + end + end end end diff --git a/app/controllers/users/webauthn/sessions_controller.rb b/app/controllers/users/webauthn/sessions_controller.rb index 412b7d50..f314902e 100644 --- a/app/controllers/users/webauthn/sessions_controller.rb +++ b/app/controllers/users/webauthn/sessions_controller.rb @@ -2,89 +2,93 @@ # frozen_string_literal: true -class Users::Webauthn::SessionsController < ApplicationController - extend T::Sig - include Authentication - - before_action :test_recaptcha, only: [:create] - skip_before_action :redirect_if_no_current_user - - def create - user = User.find_by(phone: phone) - - if user&.has_passkey? - get_options = relying_party.options_for_authentication( - allow: user.passkeys.pluck(:external_id), - user_verification: 'required' - ) - - session[:current_authentication] = { challenge: get_options.challenge, phone: phone } - - render json: get_options - elsif phone.present? - if Rails.env.production? - render json: { success: send_phone_verification(session, phone) }, status: 202 - else - session[:phone] = phone - render json: { success: true }, status: 202 +module Users + module Webauthn + class SessionsController < ApplicationController + extend T::Sig + include Authentication + + before_action :test_recaptcha, only: [:create] + skip_before_action :redirect_if_no_current_user + + def create + user = User.find_by(phone:) + + if user&.has_passkey? + get_options = relying_party.options_for_authentication( + allow: user.passkeys.pluck(:external_id), + user_verification: "required" + ) + + session[:current_authentication] = {challenge: get_options.challenge, phone:} + + render json: get_options + elsif phone.present? + if Rails.env.production? + render json: {success: send_phone_verification(session, phone)}, status: :accepted + else + session[:phone] = phone + render json: {success: true}, status: :accepted + end + else + render json: {success: false}, status: :unprocessable_entity + end end - else - render json: { success: false }, status: :unprocessable_entity - end - end - def callback - user = User.find_by(phone: session.dig(:current_authentication, 'phone')) - raise "user #{session.dig(:current_authentication, 'phone')} never initiated sign up" unless user - - begin - verified_webauthn_passkey, stored_passkey = relying_party.verify_authentication( - public_key_credential_params, - session.dig(:current_authentication, 'challenge'), - user_verification: true - ) do |webauthn_passkey| - user.passkeys.find_by(external_id: Base64.strict_encode64(webauthn_passkey.raw_id)) + def callback + user = User.find_by(phone: session.dig(:current_authentication, "phone")) + raise "user #{session.dig(:current_authentication, "phone")} never initiated sign up" unless user + + begin + verified_webauthn_passkey, stored_passkey = relying_party.verify_authentication( + public_key_credential_params, + session.dig(:current_authentication, "challenge"), + user_verification: true + ) do |webauthn_passkey| + user.passkeys.find_by(external_id: Base64.strict_encode64(webauthn_passkey.raw_id)) + end + + stored_passkey.update!(sign_count: verified_webauthn_passkey.sign_count) + sign_in(user) + + session[:verified_phone] = user.phone + if user.is_registration_complete + T.unsafe(self).route_legislators + else + T.unsafe(self).route_registration + end + rescue WebAuthn::Error => e + render json: { + success: false, + message: "Verification failed: #{e.message}" + }, status: :unprocessable_entity + ensure + session.delete(:current_authentication) + end end - stored_passkey.update!(sign_count: verified_webauthn_passkey.sign_count) - sign_in(user) + def destroy + sign_out - session[:verified_phone] = user.phone - if user.is_registration_complete - T.unsafe(self).route_legislators - else - T.unsafe(self).route_registration + # redirect_to root_path end - rescue WebAuthn::Error => e - render json: { - success: false, - message: "Verification failed: #{e.message}" - }, status: :unprocessable_entity - ensure - session.delete(:current_authentication) - end - end - - def destroy - sign_out - - # redirect_to root_path - end - private + private - def session_params - params.require(:session).permit(:phone, :publicKeyCredential, :token) - end + def session_params + params.require(:session).permit(:phone, :publicKeyCredential, :token) + end - def public_key_credential_params - # params.require(:session).require(:publicKeyCredential).permit(:type, :id, :rawId, :authenticatorAttachment, - # :response, :userHandle, :clientExtensionResults) - params.require(:session).require(:publicKeyCredential) - end + def public_key_credential_params + # params.require(:session).require(:publicKeyCredential).permit(:type, :id, :rawId, :authenticatorAttachment, + # :response, :userHandle, :clientExtensionResults) + params.require(:session).require(:publicKeyCredential) + end - sig { returns(T.nilable(String)) } - def phone - session_params[:phone]&.remove_non_digits + sig { returns(T.nilable(String)) } + def phone + session_params[:phone]&.remove_non_digits + end + end end end diff --git a/app/controllers/users/webauthn_controller.rb b/app/controllers/users/webauthn_controller.rb index aa726bd6..508964e0 100644 --- a/app/controllers/users/webauthn_controller.rb +++ b/app/controllers/users/webauthn_controller.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true # typed: strict -class Users::WebauthnController < ApplicationController + +module Users + class WebauthnController < ApplicationController + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index eb36c409..20b02730 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true # typed: strict + class UsersController < ApplicationController end diff --git a/app/frontend/components/Layout.tsx b/app/frontend/components/Layout.tsx index 1a2b20dd..3ecefe97 100644 --- a/app/frontend/components/Layout.tsx +++ b/app/frontend/components/Layout.tsx @@ -2,8 +2,6 @@ import Footer from "app/frontend/components/Footer"; import AppDrawer from "app/frontend/components/drawer/AppDrawer"; import React, { PropsWithChildren } from "react"; -// Load react-select -// @ts-expect-error - unused Select, importing here to have styles available import { Animate } from "react-simple-animate"; interface IProps extends PropsWithChildren { diff --git a/app/frontend/components/admin/creator/BillCreatorFields.tsx b/app/frontend/components/admin/creator/BillCreatorFields.tsx index f144510b..d4d528a6 100644 --- a/app/frontend/components/admin/creator/BillCreatorFields.tsx +++ b/app/frontend/components/admin/creator/BillCreatorFields.tsx @@ -318,8 +318,10 @@ const BillCreatorFields = forwardRef(({ legislators }: IProps, summaryRef: React return date; })()} selected={(values as Record)[swayField.name]} - onChange={(changed: Date) => { - setFieldValue(swayField.name, changed).catch(console.error); + onChange={(changed: Date | null) => { + if (changed) { + setFieldValue(swayField.name, changed).catch(console.error); + } }} /> diff --git a/app/frontend/components/drawer/NoUserAppDrawer.tsx b/app/frontend/components/drawer/NoUserAppDrawer.tsx deleted file mode 100644 index 53862a3e..00000000 --- a/app/frontend/components/drawer/NoUserAppDrawer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** @format */ - -import { ROUTES } from "app/frontend/sway_constants"; -import { useMemo } from "react"; -import { IconBaseProps } from "react-icons"; -import { FiBookmark, FiClock, FiLogIn, FiLogOut, FiUserPlus } from "react-icons/fi"; -import { useIsUserRegistrationComplete } from "../../hooks/users/useIsUserRegistrationComplete"; -import SwayDrawer from "./SwayDrawer"; - -type MenuItem = { - route: string; - Icon: React.FC; - text: string; -}; -const MenuChoices: MenuItem[] = [ - { route: ROUTES.billOfTheWeek, Icon: FiBookmark, text: "Bill of the Week" }, - { - route: ROUTES.pastBills, - Icon: FiClock, - text: "Past Bills of the Week", - }, -]; -const SignInChoice: MenuItem[] = [{ route: ROUTES.signin, Icon: FiLogIn, text: "Sign In" }]; -const RegistrationChoice: MenuItem[] = [ - { - route: ROUTES.registration, - Icon: FiUserPlus, - text: "Registration", - }, -]; - -interface IProps { - children: React.ReactNode; -} - -const BOTTOM_MENU_CHOICES = [ - { route: ROUTES.logout, Icon: FiLogOut, text: "Sign Out" }, -] as MenuItem[]; - -const NoUserAppDrawer: React.FC = (props) => { - const isRegistrationComplete = useIsUserRegistrationComplete(); - - const menuChoices: MenuItem[] = useMemo( - () => - isRegistrationComplete === false - ? RegistrationChoice.concat(MenuChoices) - : SignInChoice.concat(MenuChoices), - [isRegistrationComplete], - ); - - return ( - - {props.children} - - ); -}; - -export default NoUserAppDrawer; diff --git a/app/frontend/components/legislator/LegislatorCardSocialItem.tsx b/app/frontend/components/legislator/LegislatorCardSocialItem.tsx index acd70975..77453134 100644 --- a/app/frontend/components/legislator/LegislatorCardSocialItem.tsx +++ b/app/frontend/components/legislator/LegislatorCardSocialItem.tsx @@ -7,7 +7,7 @@ interface IProps { title: string; text: string; handleCopy: (text: string) => void; - Icon: React.FC; + Icon: React.ReactNode; } const Div = ({ children, ...props }: PropsWithChildren) =>
{children}
; @@ -43,7 +43,7 @@ const LegislatorCardSocialItem: React.FC = ({ title, text, handleCopy, I
diff --git a/app/frontend/components/legislator/LegislatorEmail.tsx b/app/frontend/components/legislator/LegislatorEmail.tsx index 67ece861..abe4ca40 100644 --- a/app/frontend/components/legislator/LegislatorEmail.tsx +++ b/app/frontend/components/legislator/LegislatorEmail.tsx @@ -17,7 +17,6 @@ const LegislatorEmail: React.FC = ({ legislator, handleCopy }) => { const [open, setOpen] = useState(false); const handleOpen = useCallback(() => setOpen(true), []); const handleClose = useCallback(() => setOpen(false), []); - const getIcon = useCallback(() =>