├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ └── noticed
│ │ └── web_push
│ │ └── sub_test.rb
├── controllers
│ └── .keep
├── dummy
│ ├── log
│ │ └── .keep
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── models
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_record.rb
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_controller.rb
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ ├── mailer.text.erb
│ │ │ │ ├── mailer.html.erb
│ │ │ │ └── application.html.erb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── mailers
│ │ │ └── application_mailer.rb
│ │ └── jobs
│ │ │ └── application_job.rb
│ ├── bin
│ │ ├── rake
│ │ ├── rails
│ │ └── setup
│ ├── config
│ │ ├── routes.rb
│ │ ├── environment.rb
│ │ ├── cable.yml
│ │ ├── boot.rb
│ │ ├── initializers
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── permissions_policy.rb
│ │ │ ├── assets.rb
│ │ │ ├── inflections.rb
│ │ │ └── content_security_policy.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── application.rb
│ │ ├── storage.yml
│ │ ├── puma.rb
│ │ └── environments
│ │ │ ├── test.rb
│ │ │ ├── development.rb
│ │ │ └── production.rb
│ ├── config.ru
│ └── Rakefile
├── integration
│ ├── .keep
│ └── navigation_test.rb
├── fixtures
│ ├── files
│ │ └── .keep
│ └── noticed
│ │ └── web_push
│ │ └── subscriptions.yml
├── noticed
│ └── web_push_test.rb
└── test_helper.rb
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ └── noticed
│ │ └── web_push
│ │ ├── application_record.rb
│ │ └── subscription.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ └── noticed
│ │ └── web_push
│ │ ├── application_controller.rb
│ │ ├── subscriptions_controller.rb
│ │ └── pwa_controller.rb
├── assets
│ ├── images
│ │ └── noticed
│ │ │ └── web_push
│ │ │ └── .keep
│ ├── config
│ │ └── noticed_web_push_manifest.js
│ ├── stylesheets
│ │ └── noticed
│ │ │ └── web_push
│ │ │ └── application.css
│ └── javascripts
│ │ └── noticed
│ │ └── web_push
│ │ ├── service_worker.js
│ │ └── web_push.js
├── jobs
│ └── noticed
│ │ └── web_push
│ │ └── application_job.rb
├── mailers
│ └── noticed
│ │ └── web_push
│ │ └── application_mailer.rb
├── views
│ ├── layouts
│ │ └── noticed
│ │ │ └── web_push
│ │ │ └── application.html.erb
│ └── noticed
│ │ └── web_push
│ │ └── pwa
│ │ └── app.webmanifest.erb
└── helpers
│ └── noticed
│ └── web_push
│ └── application_helper.rb
├── config
└── routes.rb
├── lib
├── noticed
│ ├── web_push
│ │ ├── version.rb
│ │ ├── rails
│ │ │ └── routes.rb
│ │ └── engine.rb
│ ├── web_push.rb
│ └── delivery_methods
│ │ └── web_push.rb
├── tasks
│ └── noticed
│ │ └── web_push_tasks.rake
└── generators
│ └── noticed
│ └── web_push
│ ├── templates
│ ├── app.webmanifest.erb.tt
│ └── README
│ ├── vapid_keys_generator.rb
│ └── install_generator.rb
├── .gitignore
├── Rakefile
├── docs
└── service_worker_gotchas.md
├── Gemfile
├── package.json
├── db
└── migrate
│ └── 20231225214350_create_noticed_web_push_subs.rb
├── bin
└── rails
├── MIT-LICENSE
├── noticed-web_push.gemspec
├── README.md
└── Gemfile.lock
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/noticed/web_push/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/assets/config/noticed_web_push_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../stylesheets/noticed/web_push .css
2 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Noticed::WebPush::Engine.routes.draw do
2 | resources :subscriptions, only: :create
3 | end
4 |
--------------------------------------------------------------------------------
/lib/noticed/web_push/version.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | VERSION = "0.1.0"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount Noticed::WebPush::Engine => "/noticed-web_push"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/tasks/noticed/web_push_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :noticed_web_push do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link noticed_web_push_manifest.js
4 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/jobs/noticed/web_push/application_job.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | class ApplicationJob < ActiveJob::Base
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/controllers/noticed/web_push/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | class ApplicationController < ActionController::Base
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/integration/navigation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class NavigationTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /doc/
3 | /log/*.log
4 | /pkg/
5 | /tmp/
6 | /test/dummy/db/*.sqlite3
7 | /test/dummy/db/*.sqlite3-*
8 | /test/dummy/log/*.log
9 | /test/dummy/storage/
10 | /test/dummy/tmp/
11 |
--------------------------------------------------------------------------------
/app/models/noticed/web_push/application_record.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/noticed/web_push_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Noticed::WebPushTest < ActiveSupport::TestCase
4 | test "it has a version number" do
5 | assert Noticed::WebPush::VERSION
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4 | load "rails/tasks/engine.rake"
5 |
6 | load "rails/tasks/statistics.rake"
7 |
8 | require "bundler/gem_tasks"
9 |
--------------------------------------------------------------------------------
/app/mailers/noticed/web_push/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: "from@example.com"
5 | layout "mailer"
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/models/noticed/web_push/sub_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | module Noticed::WebPush
4 | class SubscriptionTest < ActiveSupport::TestCase
5 | # test "the truth" do
6 | # assert true
7 | # end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/docs/service_worker_gotchas.md:
--------------------------------------------------------------------------------
1 | # Service Worker Gotchas
2 |
3 | Service workers have some gotchas to be aware of…
4 |
5 | ## Needs to be in root directory
6 |
7 | only affects child paths
8 |
9 | ## Scope option
10 |
11 | not universally supported
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/noticed/web_push/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Noticed web push
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag "noticed/web_push/application", media: "all" %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Specify your gem's dependencies in noticed-web_push.gemspec.
5 | gemspec
6 |
7 | gem "puma"
8 |
9 | gem "sqlite3"
10 |
11 | gem "sprockets-rails"
12 |
13 | # Start debugger with binding.b [https://github.com/ruby/debug]
14 | # gem "debug", ">= 1.0.0"
15 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= stylesheet_link_tag "application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/fixtures/noticed/web_push/subscriptions.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | user: one
5 | user_type: User
6 | endpoint: MyString
7 | auth_key: MyString
8 | p256dh_key: MyString
9 |
10 | two:
11 | user: two
12 | user_type: User
13 | endpoint: MyString
14 | auth_key: MyString
15 | p256dh_key: MyString
16 |
--------------------------------------------------------------------------------
/app/helpers/noticed/web_push/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | module ApplicationHelper
4 |
5 | def web_push_public_key_meta_tag(web_push_public_key: Rails.application.credentials.dig(:web_push, :public_key))
6 | tag.meta name: "web_push_public", content: Base64.urlsafe_decode64(web_push_public_key).bytes.to_json
7 | end
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "noticed-web_push",
3 | "type": "module",
4 | "version": "0.1.0",
5 | "description": "Javascript helpers for noticed-web_push",
6 | "exports": {
7 | "./*": "./app/assets/javascripts/noticed/web_push/*.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/jbennett/noticed-web_push"
12 | },
13 | "author": "Jonathan Bennett",
14 | "license": "MIT"
15 | }
--------------------------------------------------------------------------------
/db/migrate/20231225214350_create_noticed_web_push_subs.rb:
--------------------------------------------------------------------------------
1 | class CreateNoticedWebPushSubs < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :noticed_web_push_subscriptions do |t|
4 | t.references :user, polymorphic: true, null: false
5 | t.string :endpoint, null: false
6 | t.string :auth_key, null: false
7 | t.string :p256dh_key, null: false
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/noticed/web_push.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "noticed/web_push/version"
4 | require "noticed/web_push/engine"
5 | require "web_push"
6 |
7 | module Noticed
8 | module DeliveryMethods
9 | autoload :WebPush, "noticed/delivery_methods/web_push"
10 | end
11 |
12 | module WebPush
13 | autoload :Subscription, "noticed/web_push/subscription"
14 |
15 | class Error < StandardError; end
16 | # Your code goes here...
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/noticed/web_push/rails/routes.rb:
--------------------------------------------------------------------------------
1 | module ActionDispatch::Routing
2 | class Mapper
3 | def mount_web_push(path = "/web_push", skip_service_worker: false, skip_webmanifest: false)
4 | mount Noticed::WebPush::Engine => path
5 | get "/service_worker", to: "noticed/web_push/pwa#service_worker" unless skip_service_worker
6 | get "/app.webmanifest", to: "noticed/web_push/pwa#app_webmanifest", defaults: { format: :webmanifest } unless skip_webmanifest
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/noticed/web_push/pwa/app.webmanifest.erb:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OVERRIDE VIEW IN APPLICATION",
3 | "short_name": "SHORT NAME",
4 | "start_url": "/sign_in",
5 | "display": "fullscreen",
6 | "icons": [
7 | {
8 | "src": "<%= j image_path("icon-192.png") %>",
9 | "sizes": "192x192",
10 | "type": "image/png"
11 | },
12 | {
13 | "src": "<%= j image_path("icon-512.png") %>",
14 | "sizes": "512x512",
15 | "type": "image/png"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/lib/generators/noticed/web_push/templates/app.webmanifest.erb.tt:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OVERRIDE VIEW IN APPLICATION",
3 | "short_name": "SHORT NAME",
4 | "start_url": "/sign_in",
5 | "display": "fullscreen",
6 | "icons": [
7 | {
8 | "src": "<%= j image_path("icon-192.png") %>",
9 | "sizes": "192x192",
10 | "type": "image/png"
11 | },
12 | {
13 | "src": "<%= j image_path("icon-512.png") %>",
14 | "sizes": "512x512",
15 | "type": "image/png"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
4 | # Use this to limit dissemination of sensitive information.
5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/app/controllers/noticed/web_push/subscriptions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Noticed::WebPush::SubscriptionsController < ApplicationController
4 | skip_before_action :verify_authenticity_token
5 |
6 | def create
7 | Noticed::WebPush::Subscription.find_or_create_by!(user: current_user, endpoint: params[:endpoint], auth_key: params[:keys][:auth], p256dh_key: params[:keys][:p256dh])
8 |
9 | head :ok
10 | end
11 | end
12 |
13 | # TODO
14 | # - replace AppController
15 | # - replace current_user
--------------------------------------------------------------------------------
/test/dummy/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide HTTP permissions policy. For further
4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy
5 |
6 | # Rails.application.config.permissions_policy do |policy|
7 | # policy.camera :none
8 | # policy.gyroscope :none
9 | # policy.microphone :none
10 | # policy.usb :none
11 | # policy.fullscreen :self
12 | # policy.payment :self, "https://secure.example.com"
13 | # end
14 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/lib/noticed/web_push/engine.rb:
--------------------------------------------------------------------------------
1 | # TODO why is this not autoloading
2 | require_relative "../../../app/helpers/noticed/web_push/application_helper"
3 | require_relative "rails/routes"
4 |
5 | module Noticed
6 | module WebPush
7 | class Engine < ::Rails::Engine
8 | isolate_namespace Noticed::WebPush
9 |
10 | ActiveSupport.on_load(:action_view) do
11 | include Noticed::WebPush::ApplicationHelper
12 | end
13 |
14 | config.after_initialize do
15 | Mime::Type.register "application/manifest+json", :webmanifest
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path("..", __dir__)
6 | ENGINE_PATH = File.expand_path("../lib/noticed/web_push/engine", __dir__)
7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12 |
13 | require "rails/all"
14 | require "rails/engine/commands"
15 |
--------------------------------------------------------------------------------
/app/controllers/noticed/web_push/pwa_controller.rb:
--------------------------------------------------------------------------------
1 | class Noticed::WebPush::PwaController < ApplicationController
2 | skip_before_action :verify_authenticity_token
3 |
4 | def service_worker
5 | send_file get_full_path_to_asset("service_worker.js"), disposition: :inline
6 | end
7 |
8 | def app_webmanifest
9 | render :app
10 | end
11 |
12 | private
13 |
14 | def get_full_path_to_asset(filename)
15 | manifest_file = Rails.application.assets_manifest.assets[filename]
16 | if manifest_file
17 | File.join(Rails.application.assets_manifest.directory, manifest_file)
18 | else
19 | Rails.application.assets&.[](filename)&.filename
20 | end
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/lib/generators/noticed/web_push/vapid_keys_generator.rb:
--------------------------------------------------------------------------------
1 | require "rails/generators/named_base"
2 |
3 | module Noticed
4 | module WebPush
5 | module Generators
6 | class VapidKeysGenerator < Rails::Generators::Base
7 | def generate_vapid_keys
8 | puts <<~KEYS
9 | Add the following to your credentials (rails credentials:edit):"
10 |
11 | web_push:
12 | public_key: "#{vapid_key.public_key}"
13 | private_key: "#{vapid_key.private_key}"
14 | KEYS
15 | end
16 |
17 | private
18 |
19 | def vapid_key
20 | @vapid_key ||= ::WebPush.generate_key
21 | end
22 | end
23 | end
24 | end
25 | end
--------------------------------------------------------------------------------
/lib/generators/noticed/web_push/templates/README:
--------------------------------------------------------------------------------
1 | Next Steps:
2 |
3 | 1. Add a trigger to prompt for push notifications: ``
4 | 2. Customize `public/web_push_app.webmanifest` for your your application
5 | 3. Customize `public/web_push_service_worker.js` for your your application
6 | 4. Install certificate on mobile device
7 | 1. Download and add localhost.crt onto your mobile device
8 | 2. Install and trust [iOS](https://support.apple.com/en-us/HT204477) | [Android](https://proxyman.io/posts/2020-09-29-Install-And-Trust-Self-Signed-Certificate-On-Android-11)
9 | 3. Skipping this will cause the notification prompt to always be denied
10 |
--------------------------------------------------------------------------------
/app/models/noticed/web_push/subscription.rb:
--------------------------------------------------------------------------------
1 | module Noticed::WebPush
2 | class Subscription < ActiveRecord::Base
3 | belongs_to :user, polymorphic: true
4 | encrypts :endpoint, deterministic: true
5 | encrypts :auth_key, deterministic: true
6 | encrypts :p256dh_key, deterministic: true
7 |
8 | def publish(data)
9 | WebPush.payload_send(
10 | message: data.to_json,
11 | endpoint: endpoint,
12 | p256dh: p256dh_key,
13 | auth: auth_key,
14 | vapid: {
15 | private_key: Rails.application.credentials.dig(:web_push, :private_key),
16 | public_key: Rails.application.credentials.dig(:web_push, :public_key)
17 | }
18 | )
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: storage/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: storage/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: storage/production.sqlite3
26 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../test/dummy/config/environment"
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
6 | ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
7 | require "rails/test_help"
8 |
9 | # Load fixtures from the engine
10 | if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
11 | ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)]
12 | ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
13 | ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files"
14 | ActiveSupport::TestCase.fixtures :all
15 | end
16 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/web_push.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class WebPush < Noticed::DeliveryMethods::Base
4 | option :data_method
5 |
6 | def deliver
7 | recipient.web_push_subscriptions.each do |subscription|
8 | subscription.publish(data)
9 | rescue ::WebPush::ExpiredSubscription
10 | Rails.logger.info "Removing expired WebPush subscription"
11 | subscription.destroy
12 | rescue ::WebPush::Unauthorized
13 | Rails.logger.info "Removing unauthorized WebPush subscription"
14 | subscription.destroy
15 | end
16 | end
17 |
18 | private
19 |
20 | def data
21 | notification.send(options.fetch(:data_method, :web_push_data))
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/noticed/web_push/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization and
2 | # are automatically loaded by Rails. If you want to use locales other than
3 | # English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more about the API, please read the Rails Internationalization guide
20 | # at https://guides.rubyonrails.org/i18n.html.
21 | #
22 | # Be aware that YAML interprets the following case-insensitive strings as
23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24 | # must be quoted to be interpreted as strings. For example:
25 | #
26 | # en:
27 | # "yes": yup
28 | # enabled: "ON"
29 |
30 | en:
31 | hello: "Hello world"
32 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Jonathan Bennett
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args, exception: true)
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Dummy
10 | class Application < Rails::Application
11 | config.load_defaults Rails::VERSION::STRING.to_f
12 |
13 | # For compatibility with applications that use this config
14 | config.action_controller.include_all_helpers = false
15 |
16 | # Please, add to the `ignore` list any other `lib` subdirectories that do
17 | # not contain `.rb` files, or that should not be reloaded or eager loaded.
18 | # Common ones are `templates`, `generators`, or `middleware`, for example.
19 | config.autoload_lib(ignore: %w(assets tasks))
20 |
21 | # Configuration for the application, engines, and railties goes here.
22 | #
23 | # These settings can be overridden in specific environments using the files
24 | # in config/environments, which are processed later.
25 | #
26 | # config.time_zone = "Central Time (US & Canada)"
27 | # config.eager_load_paths << Rails.root.join("extras")
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/assets/javascripts/noticed/web_push/service_worker.js:
--------------------------------------------------------------------------------
1 | ENGINE_MOUNT_PATH = null
2 | VERSION = 1
3 |
4 | export function start(options) {
5 | ENGINE_MOUNT_PATH = options.mounted_path
6 | }
7 |
8 | self.addEventListener('push', event => {
9 | const data = event.data?.json() || {}
10 | console.log(`v${VERSION}: Received push`, data)
11 |
12 | event.waitUntil(
13 | self.registration.showNotification(data.title, {
14 | body: data.body,
15 | data: data,
16 | })
17 | )
18 | })
19 |
20 | self.addEventListener("notificationclick", event => {
21 | event.notification.close()
22 | console.log(`opening ${event.notification.data.url}`)
23 |
24 | event.waitUntil(
25 | self.clients.openWindow(event.notification.data.url)
26 | )
27 | })
28 |
29 | self.addEventListener('pushsubscriptionchange', async (event) => {
30 | const subscription = await self.registration.pushManager.getSubscription()
31 | await fetch(`${ENGINE_MOUNT_PATH}/subscriptions`, {
32 | method: "POST",
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify(subscription),
37 | })
38 | })
--------------------------------------------------------------------------------
/test/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src style-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/noticed-web_push.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/noticed/web_push/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "noticed-web_push"
5 | spec.version = Noticed::WebPush::VERSION
6 | spec.authors = ["Jonathan Bennett"]
7 | spec.email = ["jonathan@jbennett.me"]
8 | spec.homepage = "https://github.com/jbennett/noticed-web_push"
9 | spec.summary = "Web Push delivery support for noticed."
10 | spec.description = spec.summary
11 | spec.license = "MIT"
12 |
13 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
14 | # to allow pushing to a single host or delete this section to allow pushing to any host.
15 | # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16 |
17 | spec.metadata["homepage_uri"] = spec.homepage
18 | spec.metadata["source_code_uri"] = spec.homepage
19 | spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
20 |
21 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
22 | Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
23 | end
24 |
25 | spec.add_dependency "noticed", "~> 1.0"
26 | spec.add_dependency "web-push", "~> 3.0"
27 | end
28 |
--------------------------------------------------------------------------------
/test/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Noticed::WebPush
2 |
3 | ## 🎉 Web Push Notifications for your Ruby on Rails app.
4 |
5 | [🎬 Screencast](https://www.youtube.com/watch?v=-9KWx7Pj5AM)
6 |
7 | ## 🚀 Installation
8 |
9 | Run the following commands to add Noticed-WebPush to your Gemfile and install the engine.
10 |
11 | ```bash
12 | bundle add noticed-web_push --github jbennett/noticed-web_push
13 |
14 | rails g noticed:web_push:install
15 | rails noticed_web_push:install:migrations
16 | rails db:migrate
17 | ```
18 |
19 | Generate and save encryption keys. Do this for all your environments.
20 |
21 | ```bash
22 | rails g noticed:web_push:vapid_keys
23 | rails credentials:edit
24 | ```
25 |
26 | ## 📝 Usage
27 |
28 | Add the `web_push` delivery method to your existing notifications
29 |
30 | ```ruby
31 | class NewPostNotification < Noticed::Base
32 | deliver_by :database
33 | deliver_by :web_push, data_method: :web_push_data
34 |
35 | param :post
36 |
37 | def post
38 | params[:post]
39 | end
40 |
41 | def web_push_data
42 | {
43 | title: "New post: #{post.title}",
44 | body: post.content.truncate(40),
45 | url: post_url(post),
46 | }
47 | end
48 | end
49 | ```
50 |
51 | ## How it works
52 |
53 | **add details about service_worker paths etc and how the manifest etc are special**
54 |
55 |
56 | ## License
57 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
58 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # This configuration file will be evaluated by Puma. The top-level methods that
2 | # are invoked here are part of Puma's configuration DSL. For more information
3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4 |
5 | # Puma can serve each request in a thread from an internal thread pool.
6 | # The `threads` method setting takes two numbers: a minimum and maximum.
7 | # Any libraries that use thread pools should be configured to match
8 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
9 | # and maximum; this matches the default thread size of Active Record.
10 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
11 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
12 | threads min_threads_count, max_threads_count
13 |
14 | # Specifies that the worker count should equal the number of processors in production.
15 | if ENV["RAILS_ENV"] == "production"
16 | require "concurrent-ruby"
17 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
18 | workers worker_count if worker_count > 1
19 | end
20 |
21 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
22 | # terminating a worker in development environments.
23 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
24 |
25 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
26 | port ENV.fetch("PORT") { 3000 }
27 |
28 | # Specifies the `environment` that Puma will run in.
29 | environment ENV.fetch("RAILS_ENV") { "development" }
30 |
31 | # Specifies the `pidfile` that Puma will use.
32 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
33 |
34 | # Allow puma to be restarted by `bin/rails restart` command.
35 | plugin :tmp_restart
36 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/lib/generators/noticed/web_push/install_generator.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module WebPush
3 | module Generators
4 | class InstallGenerator < Rails::Generators::Base
5 | source_root File.expand_path("../templates", __FILE__)
6 |
7 | def add_javascript_deps
8 | run "yarn add github:jbennett/noticed-web_push"
9 | append_to_file File.join("app", "javascript", "application.js") do
10 | <<~INIT
11 |
12 | import * as web_push from "noticed-web_push/web_push"
13 | web_push.start({
14 | mounted_path: "/web_push",
15 | server_worker_path: "/service_worker.js"
16 | })
17 | INIT
18 | end
19 | end
20 |
21 | def setup_service_worker
22 | run "touch app/javascript/service_worker.js"
23 | append_to_file File.join("app", "javascript", "service_worker.js") do
24 | <<~INIT
25 | import {start} from "noticed-web_push/service_worker"
26 | start({
27 | mounted_path: "/web_push",
28 | })
29 | INIT
30 | end
31 | append_to_file "config/initializers/assets.rb", "Rails.application.config.assets.precompile += %w( service_worker.js )"
32 | end
33 |
34 | def create_webmanifest
35 | template "app.webmanifest.erb.tt", "app/noticed/web_push/pwa/app.webmanifest.erb"
36 | end
37 |
38 | def update_rubies
39 | inject_into_file "app/models/user.rb", after: "has_many :notifications, as: :recipient, dependent: :destroy\n" do
40 | "\thas_many :web_push_subscriptions, class_name: \"Noticed::WebPush::Subscription\", dependent: :destroy\n"
41 | end
42 | route "mount_web_push\n"
43 |
44 | inject_into_file "app/views/layouts/application.html.erb", before: "" do
45 | <<~INIT
46 |
47 | \t\t<%= tag.link rel: :manifest, href: "/app.webmanifest" %>
48 | \t\t<%= web_push_public_key_meta_tag %>
49 | INIT
50 | end
51 | inject_into_file "app/views/layouts/application.html.erb", after: "\n" do
52 | "\t\t\n"
53 | end
54 | end
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/app/assets/javascripts/noticed/web_push/web_push.js:
--------------------------------------------------------------------------------
1 | ENGINE_MOUNT_PATH = null
2 | SERVICE_WORKER_PATH = null
3 |
4 | export function start(options) {
5 | ENGINE_MOUNT_PATH = options.mounted_path
6 | SERVICE_WORKER_PATH = options.server_worker_path
7 |
8 | document.addEventListener("turbo:load", () => {
9 | switch (window.Notification?.permission) {
10 | case null:
11 | return // not supported
12 | case "granted":
13 | saveSubscription()
14 | return
15 | case "denied":
16 | return // do nothing?
17 | default:
18 | promptForNotifications()
19 | }
20 | })
21 | }
22 |
23 | function promptForNotifications() {
24 | const notificationsButton = document.getElementById("enable_notifications")
25 | if (!notificationsButton) return
26 |
27 | notificationsButton.style.display = null
28 | notificationsButton.addEventListener("click", event => {
29 | event.preventDefault()
30 | Notification.requestPermission()
31 | .then((permission) => {
32 | if (permission === "granted") {
33 | setupSubscription()
34 | } else {
35 | alert("Notifications declined")
36 | }
37 | })
38 | .catch(error => console.log("Notifications error", error))
39 | .finally(() => notificationsButton.style.display = "none")
40 | })
41 | }
42 |
43 | async function setupSubscription() {
44 | if (window.Notification?.permission !== "granted") return
45 | if (!navigator.serviceWorker) return
46 |
47 | let key_bytes = document.querySelector("meta[name=web_push_public]")?.content
48 | let vapid = new Uint8Array(JSON.parse(key_bytes))
49 |
50 | await navigator.serviceWorker.register(SERVICE_WORKER_PATH)
51 | const registration = await navigator.serviceWorker.ready
52 | await registration.pushManager.subscribe({
53 | userVisibleOnly: true,
54 | applicationServerKey: vapid
55 | })
56 |
57 | await saveSubscription()
58 | }
59 |
60 | async function saveSubscription() {
61 | if (window.Notification?.permission !== "granted") return
62 | if (!navigator.serviceWorker) return
63 |
64 | const registration = await navigator.serviceWorker.ready
65 | const subscription = await registration.pushManager.getSubscription()
66 | if (!subscription) return
67 |
68 | await fetch(`${ENGINE_MOUNT_PATH}/subscriptions`, {
69 | method: "POST",
70 | headers: {
71 | "Content-Type": "application/json",
72 | },
73 | body: JSON.stringify(subscription)
74 | })
75 | }
76 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # While tests run files are not watched, reloading is not necessary.
12 | config.enable_reloading = false
13 |
14 | # Eager loading loads your entire application. When running a single test locally,
15 | # this is usually not necessary, and can slow down your test suite. However, it's
16 | # recommended that you enable it in continuous integration systems to ensure eager
17 | # loading is working properly before deploying your code.
18 | config.eager_load = ENV["CI"].present?
19 |
20 | # Configure public file server for tests with Cache-Control for performance.
21 | config.public_file_server.enabled = true
22 | config.public_file_server.headers = {
23 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
24 | }
25 |
26 | # Show full error reports and disable caching.
27 | config.consider_all_requests_local = true
28 | config.action_controller.perform_caching = false
29 | config.cache_store = :null_store
30 |
31 | # Render exception templates for rescuable exceptions and raise for other exceptions.
32 | config.action_dispatch.show_exceptions = :rescuable
33 |
34 | # Disable request forgery protection in test environment.
35 | config.action_controller.allow_forgery_protection = false
36 |
37 | # Store uploaded files on the local file system in a temporary directory.
38 | config.active_storage.service = :test
39 |
40 | config.action_mailer.perform_caching = false
41 |
42 | # Tell Action Mailer not to deliver emails to the real world.
43 | # The :test delivery method accumulates sent emails in the
44 | # ActionMailer::Base.deliveries array.
45 | config.action_mailer.delivery_method = :test
46 |
47 | # Print deprecation notices to the stderr.
48 | config.active_support.deprecation = :stderr
49 |
50 | # Raise exceptions for disallowed deprecations.
51 | config.active_support.disallowed_deprecation = :raise
52 |
53 | # Tell Active Support which deprecation messages to disallow.
54 | config.active_support.disallowed_deprecation_warnings = []
55 |
56 | # Raises error for missing translations.
57 | # config.i18n.raise_on_missing_translations = true
58 |
59 | # Annotate rendered view with file names.
60 | # config.action_view.annotate_rendered_view_with_filenames = true
61 |
62 | # Raise error when a before_action's only/except options reference missing actions
63 | config.action_controller.raise_on_missing_callback_actions = true
64 | end
65 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.enable_reloading = true
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | # Print deprecation notices to the Rails logger.
45 | config.active_support.deprecation = :log
46 |
47 | # Raise exceptions for disallowed deprecations.
48 | config.active_support.disallowed_deprecation = :raise
49 |
50 | # Tell Active Support which deprecation messages to disallow.
51 | config.active_support.disallowed_deprecation_warnings = []
52 |
53 | # Raise an error on page load if there are pending migrations.
54 | config.active_record.migration_error = :page_load
55 |
56 | # Highlight code that triggered database queries in logs.
57 | config.active_record.verbose_query_logs = true
58 |
59 | # Highlight code that enqueued background job in logs.
60 | config.active_job.verbose_enqueue_logs = true
61 |
62 | # Suppress logger output for asset requests.
63 | config.assets.quiet = true
64 |
65 | # Raises error for missing translations.
66 | # config.i18n.raise_on_missing_translations = true
67 |
68 | # Annotate rendered view with file names.
69 | # config.action_view.annotate_rendered_view_with_filenames = true
70 |
71 | # Uncomment if you wish to allow Action Cable access from any origin.
72 | # config.action_cable.disable_request_forgery_protection = true
73 |
74 | # Raise error when a before_action's only/except options reference missing actions
75 | config.action_controller.raise_on_missing_callback_actions = true
76 | end
77 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.enable_reloading = false
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment
20 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
24 | # config.public_file_server.enabled = false
25 |
26 | # Compress CSS using a preprocessor.
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
33 | # config.asset_host = "http://assets.example.com"
34 |
35 | # Specifies the header that your server uses for sending files.
36 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
37 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
38 |
39 | # Store uploaded files on the local file system (see config/storage.yml for options).
40 | config.active_storage.service = :local
41 |
42 | # Mount Action Cable outside main process or domain.
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = "wss://example.com/cable"
45 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
46 |
47 | # Assume all access to the app is happening through a SSL-terminating reverse proxy.
48 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
49 | # config.assume_ssl = true
50 |
51 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
52 | config.force_ssl = true
53 |
54 | # Log to STDOUT by default
55 | config.logger = ActiveSupport::Logger.new(STDOUT)
56 | .tap { |logger| logger.formatter = ::Logger::Formatter.new }
57 | .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
58 |
59 | # Prepend all log lines with the following tags.
60 | config.log_tags = [ :request_id ]
61 |
62 | # Info include generic and useful information about system operation, but avoids logging too much
63 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you
64 | # want to log everything, set the level to "debug".
65 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
66 |
67 | # Use a different cache store in production.
68 | # config.cache_store = :mem_cache_store
69 |
70 | # Use a real queuing backend for Active Job (and separate queues per environment).
71 | # config.active_job.queue_adapter = :resque
72 | # config.active_job.queue_name_prefix = "dummy_production"
73 |
74 | config.action_mailer.perform_caching = false
75 |
76 | # Ignore bad email addresses and do not raise email delivery errors.
77 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
78 | # config.action_mailer.raise_delivery_errors = false
79 |
80 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
81 | # the I18n.default_locale when a translation cannot be found).
82 | config.i18n.fallbacks = true
83 |
84 | # Don't log any deprecations.
85 | config.active_support.report_deprecations = false
86 |
87 | # Do not dump schema after migrations.
88 | config.active_record.dump_schema_after_migration = false
89 |
90 | # Enable DNS rebinding protection and other `Host` header attacks.
91 | # config.hosts = [
92 | # "example.com", # Allow requests from example.com
93 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
94 | # ]
95 | # Skip DNS rebinding protection for the default health check endpoint.
96 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
97 | end
98 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | noticed-web_push (0.1.0)
5 | noticed (~> 1.0)
6 | web-push (~> 3.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actioncable (7.1.2)
12 | actionpack (= 7.1.2)
13 | activesupport (= 7.1.2)
14 | nio4r (~> 2.0)
15 | websocket-driver (>= 0.6.1)
16 | zeitwerk (~> 2.6)
17 | actionmailbox (7.1.2)
18 | actionpack (= 7.1.2)
19 | activejob (= 7.1.2)
20 | activerecord (= 7.1.2)
21 | activestorage (= 7.1.2)
22 | activesupport (= 7.1.2)
23 | mail (>= 2.7.1)
24 | net-imap
25 | net-pop
26 | net-smtp
27 | actionmailer (7.1.2)
28 | actionpack (= 7.1.2)
29 | actionview (= 7.1.2)
30 | activejob (= 7.1.2)
31 | activesupport (= 7.1.2)
32 | mail (~> 2.5, >= 2.5.4)
33 | net-imap
34 | net-pop
35 | net-smtp
36 | rails-dom-testing (~> 2.2)
37 | actionpack (7.1.2)
38 | actionview (= 7.1.2)
39 | activesupport (= 7.1.2)
40 | nokogiri (>= 1.8.5)
41 | racc
42 | rack (>= 2.2.4)
43 | rack-session (>= 1.0.1)
44 | rack-test (>= 0.6.3)
45 | rails-dom-testing (~> 2.2)
46 | rails-html-sanitizer (~> 1.6)
47 | actiontext (7.1.2)
48 | actionpack (= 7.1.2)
49 | activerecord (= 7.1.2)
50 | activestorage (= 7.1.2)
51 | activesupport (= 7.1.2)
52 | globalid (>= 0.6.0)
53 | nokogiri (>= 1.8.5)
54 | actionview (7.1.2)
55 | activesupport (= 7.1.2)
56 | builder (~> 3.1)
57 | erubi (~> 1.11)
58 | rails-dom-testing (~> 2.2)
59 | rails-html-sanitizer (~> 1.6)
60 | activejob (7.1.2)
61 | activesupport (= 7.1.2)
62 | globalid (>= 0.3.6)
63 | activemodel (7.1.2)
64 | activesupport (= 7.1.2)
65 | activerecord (7.1.2)
66 | activemodel (= 7.1.2)
67 | activesupport (= 7.1.2)
68 | timeout (>= 0.4.0)
69 | activestorage (7.1.2)
70 | actionpack (= 7.1.2)
71 | activejob (= 7.1.2)
72 | activerecord (= 7.1.2)
73 | activesupport (= 7.1.2)
74 | marcel (~> 1.0)
75 | activesupport (7.1.2)
76 | base64
77 | bigdecimal
78 | concurrent-ruby (~> 1.0, >= 1.0.2)
79 | connection_pool (>= 2.2.5)
80 | drb
81 | i18n (>= 1.6, < 2)
82 | minitest (>= 5.1)
83 | mutex_m
84 | tzinfo (~> 2.0)
85 | addressable (2.8.6)
86 | public_suffix (>= 2.0.2, < 6.0)
87 | base64 (0.2.0)
88 | bigdecimal (3.1.5)
89 | builder (3.2.4)
90 | concurrent-ruby (1.2.2)
91 | connection_pool (2.4.1)
92 | crass (1.0.6)
93 | date (3.3.4)
94 | domain_name (0.6.20231109)
95 | drb (2.2.0)
96 | ruby2_keywords
97 | erubi (1.12.0)
98 | ffi (1.16.3)
99 | ffi-compiler (1.0.1)
100 | ffi (>= 1.0.0)
101 | rake
102 | globalid (1.2.1)
103 | activesupport (>= 6.1)
104 | http (5.1.1)
105 | addressable (~> 2.8)
106 | http-cookie (~> 1.0)
107 | http-form_data (~> 2.2)
108 | llhttp-ffi (~> 0.4.0)
109 | http-cookie (1.0.5)
110 | domain_name (~> 0.5)
111 | http-form_data (2.3.0)
112 | i18n (1.14.1)
113 | concurrent-ruby (~> 1.0)
114 | io-console (0.7.1)
115 | irb (1.11.0)
116 | rdoc
117 | reline (>= 0.3.8)
118 | jwt (2.7.1)
119 | llhttp-ffi (0.4.0)
120 | ffi-compiler (~> 1.0)
121 | rake (~> 13.0)
122 | loofah (2.22.0)
123 | crass (~> 1.0.2)
124 | nokogiri (>= 1.12.0)
125 | mail (2.8.1)
126 | mini_mime (>= 0.1.1)
127 | net-imap
128 | net-pop
129 | net-smtp
130 | marcel (1.0.2)
131 | mini_mime (1.1.5)
132 | minitest (5.20.0)
133 | mutex_m (0.2.0)
134 | net-imap (0.4.9)
135 | date
136 | net-protocol
137 | net-pop (0.1.2)
138 | net-protocol
139 | net-protocol (0.2.2)
140 | timeout
141 | net-smtp (0.4.0)
142 | net-protocol
143 | nio4r (2.7.0)
144 | nokogiri (1.15.5-arm64-darwin)
145 | racc (~> 1.4)
146 | noticed (1.6.3)
147 | http (>= 4.0.0)
148 | rails (>= 5.2.0)
149 | openssl (3.2.0)
150 | psych (5.1.2)
151 | stringio
152 | public_suffix (5.0.4)
153 | puma (6.4.0)
154 | nio4r (~> 2.0)
155 | racc (1.7.3)
156 | rack (3.0.8)
157 | rack-session (2.0.0)
158 | rack (>= 3.0.0)
159 | rack-test (2.1.0)
160 | rack (>= 1.3)
161 | rackup (2.1.0)
162 | rack (>= 3)
163 | webrick (~> 1.8)
164 | rails (7.1.2)
165 | actioncable (= 7.1.2)
166 | actionmailbox (= 7.1.2)
167 | actionmailer (= 7.1.2)
168 | actionpack (= 7.1.2)
169 | actiontext (= 7.1.2)
170 | actionview (= 7.1.2)
171 | activejob (= 7.1.2)
172 | activemodel (= 7.1.2)
173 | activerecord (= 7.1.2)
174 | activestorage (= 7.1.2)
175 | activesupport (= 7.1.2)
176 | bundler (>= 1.15.0)
177 | railties (= 7.1.2)
178 | rails-dom-testing (2.2.0)
179 | activesupport (>= 5.0.0)
180 | minitest
181 | nokogiri (>= 1.6)
182 | rails-html-sanitizer (1.6.0)
183 | loofah (~> 2.21)
184 | nokogiri (~> 1.14)
185 | railties (7.1.2)
186 | actionpack (= 7.1.2)
187 | activesupport (= 7.1.2)
188 | irb
189 | rackup (>= 1.0.0)
190 | rake (>= 12.2)
191 | thor (~> 1.0, >= 1.2.2)
192 | zeitwerk (~> 2.6)
193 | rake (13.1.0)
194 | rdoc (6.6.2)
195 | psych (>= 4.0.0)
196 | reline (0.4.1)
197 | io-console (~> 0.5)
198 | ruby2_keywords (0.0.5)
199 | sprockets (4.2.1)
200 | concurrent-ruby (~> 1.0)
201 | rack (>= 2.2.4, < 4)
202 | sprockets-rails (3.4.2)
203 | actionpack (>= 5.2)
204 | activesupport (>= 5.2)
205 | sprockets (>= 3.0.0)
206 | sqlite3 (1.6.9-arm64-darwin)
207 | stringio (3.1.0)
208 | thor (1.3.0)
209 | timeout (0.4.1)
210 | tzinfo (2.0.6)
211 | concurrent-ruby (~> 1.0)
212 | web-push (3.0.1)
213 | jwt (~> 2.0)
214 | openssl (~> 3.0)
215 | webrick (1.8.1)
216 | websocket-driver (0.7.6)
217 | websocket-extensions (>= 0.1.0)
218 | websocket-extensions (0.1.5)
219 | zeitwerk (2.6.12)
220 |
221 | PLATFORMS
222 | arm64-darwin-22
223 |
224 | DEPENDENCIES
225 | noticed-web_push!
226 | puma
227 | sprockets-rails
228 | sqlite3
229 |
230 | BUNDLED WITH
231 | 2.4.14
232 |
--------------------------------------------------------------------------------