Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
d87f6ed
Add action_push_native gem and core SaaS infrastructure
olivaresf Jan 14, 2026
6a72631
Add User::Devices concern for native push notifications
olivaresf Jan 14, 2026
6c295b6
Add device registration API and UI for native push
olivaresf Jan 14, 2026
48cc4ae
Add NotificationPusher native push integration and tests
olivaresf Jan 14, 2026
deabebd
Add action_push_native_devices table
olivaresf Jan 15, 2026
40cdb32
Update init so ActionNativePush picks up the config
olivaresf Jan 15, 2026
98683cb
Update to add a global identifier to device tokens
olivaresf Jan 15, 2026
7653eff
Update push destination
olivaresf Jan 15, 2026
27b7829
Update schema version to match latest migration
olivaresf Jan 15, 2026
5207214
Reuse push_to_user instead of duplicating web push logic
olivaresf Jan 15, 2026
8ae9540
Scope device tokens to users for better security
olivaresf Jan 15, 2026
9424e91
Add test for dual web and native push delivery
olivaresf Jan 15, 2026
8860044
Consolidate device migrations into single migration
olivaresf Jan 15, 2026
2746e4d
Move native devices partial to SaaS engine
olivaresf Jan 15, 2026
fe560bb
Remove code comments
olivaresf Jan 15, 2026
5f5956b
Rename push_to_user to push_to_web
olivaresf Jan 15, 2026
f6f1e7e
Update with team identifiers
olivaresf Jan 16, 2026
517745e
Add script to load certificate from 1Password
olivaresf Jan 16, 2026
872aa2d
Fix NOT NULL crash in device registration
olivaresf Jan 16, 2026
58e670c
Fix owner_id type to UUID in devices migration
olivaresf Jan 16, 2026
a1e4b02
Allow enabling native push in dev via ENABLE_NATIVE_PUSH env var
olivaresf Jan 16, 2026
2f0bf24
Add --apns flag to bin/dev for loading APNs credentials
olivaresf Jan 16, 2026
3b1e002
Auto-enable SaaS mode when running bin/dev --apns
olivaresf Jan 16, 2026
1824d2e
Update bin/dev to correctly load apns
olivaresf Jan 16, 2026
db28aab
Use prepend to override functions and update initializer for fizzy-saas
olivaresf Jan 16, 2026
eb6c53f
Add missing table
olivaresf Jan 16, 2026
a0d6cb8
Update the parsing of the certificate via 1Password
olivaresf Jan 16, 2026
77f5638
Update config/push.yml
olivaresf Jan 16, 2026
681827f
Update secrets to fetch APNS from 1Password
olivaresf Jan 16, 2026
04bc368
Clean up devices controller
olivaresf Jan 16, 2026
c384022
Merge remote-tracking branch 'origin/main' into saas-push-notifications
olivaresf Jan 20, 2026
6153a0e
Update local script to load Firebase key
olivaresf Jan 20, 2026
55331c2
Update Firebase projectId in push.yml
olivaresf Jan 20, 2026
70aefca
Update name of the private key
olivaresf Jan 20, 2026
d1e47ec
Use version of `action_push_native` with proper config paths support
rosa Jan 20, 2026
d353f5c
Remove UUID requirement from push notification device registration
olivaresf Jan 21, 2026
f9afbec
Add title/body to android notifications too
olivaresf Jan 21, 2026
fc588f9
Merge branch 'saas-push-notifications' of github.com:olivaresf/fizzy …
olivaresf Jan 21, 2026
cc2e7f8
Remove UUID requirement from push notification device registration
olivaresf Jan 21, 2026
3cc7580
Send the URL instead of path in notifications
olivaresf Jan 21, 2026
9f5113c
Merge remote changes, keep UUID removal and use url instead of path
olivaresf Jan 21, 2026
ab1356b
Refactor devices controller and extract registration to model
rosa Jan 20, 2026
1ac09a2
Simplify device routes and use ActiveRecord validations
rosa Jan 21, 2026
47c360c
Change device ownership from User to Identity
rosa Jan 21, 2026
8cd6427
Refactor notification push system with registry pattern
rosa Jan 21, 2026
5639e86
Extract payload building into dedicated classes
rosa Jan 21, 2026
95a755c
Move payload method to Notification and make accessors public
rosa Jan 21, 2026
5674abd
Rename Push to PushTarget for better readability
rosa Jan 21, 2026
e0107b0
Link devices to sessions for automatic cleanup on logout
rosa Jan 21, 2026
f1d2fe4
Tidy up saas engine a bit more
rosa Jan 21, 2026
06878ac
Fix reference to `user.devices`, left-over from the identity switch
rosa Jan 21, 2026
f7695d6
Remove redundant owner index from devices table
rosa Jan 21, 2026
1062bb4
Squash device migrations into single table creation
rosa Jan 21, 2026
2d251e1
Merge branch 'main' of https://github.com/basecamp/fizzy into saas-pu…
olivaresf Jan 21, 2026
45bc451
Merge branch 'saas-push-notifications' of github.com:olivaresf/fizzy …
olivaresf Jan 21, 2026
ad9ff21
Remove foreign key constraint from devices to sessions
rosa Jan 21, 2026
947d28d
Merge branch 'saas-push-notifications' of github.com:olivaresf/fizzy …
olivaresf Jan 22, 2026
1ee47cd
Change priority notification level for mentions and assignments
olivaresf Jan 22, 2026
bab4ee0
Add unit tests
olivaresf Jan 22, 2026
549df0d
Update unit tests
olivaresf Jan 22, 2026
167776a
Change priority notification level for mentions and assignments
olivaresf Jan 22, 2026
ee05044
Make devices controller untenanted with engine routes
rosa Jan 22, 2026
0eb1b1e
Move devices table to saas database
rosa Jan 22, 2026
b63940c
Move push priority concerns from Event and Mention into Native push t…
rosa Jan 22, 2026
9029121
Move category and high_priority to payload classes
rosa Jan 22, 2026
7f51f99
Consolidate push jobs into single Notification::PushJob
rosa Jan 22, 2026
0ac7da1
Rename push to process on PushTarget for clearer semantics
rosa Jan 22, 2026
7d8b6ec
Simplify PushTarget by removing template method pattern
rosa Jan 22, 2026
a12bfd3
Add `FCM_ENCRYPTION_KEY` to Kamal secrets and organize them
rosa Jan 22, 2026
3629f94
Store `APNS_ENCRYPTION_KEY` and `FCM_ENCRYPTION_KEY` at the root
rosa Jan 22, 2026
0619d79
Merge branch 'saas-push-notifications' of github.com:olivaresf/fizzy …
olivaresf Jan 23, 2026
eff16cc
Move encryption keys to base64 password fields
rosa Jan 23, 2026
c513278
Merge branch 'main' into saas-push-notifications
rosa Jan 23, 2026
2355aa2
Fix stuck state when permission granted but no subscription
rosa Jan 23, 2026
64ff217
Wait for service worker to be active before subscribing
rosa Jan 23, 2026
2583be9
Fix notification click URL by using correct data property
rosa Jan 23, 2026
fd9f7a0
Go back to RubyGems version of `action_push_native`
rosa Jan 23, 2026
b8d5e51
Add avatar_url so we always get an avatar even when the user hasn't s…
olivaresf Jan 26, 2026
f460d8b
Add creator initials and avatar color to push notification payload
olivaresf Jan 26, 2026
d2da108
Add creator_familiar_name to push notification payload
olivaresf Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ GEM
specs:
action_text-trix (2.1.16)
railties
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
autotuner (1.1.0)
aws-eventstream (1.4.0)
Expand Down Expand Up @@ -327,7 +327,7 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.2)
puma (7.1.0)
nio4r (~> 2.0)
raabro (1.4.0)
Expand All @@ -351,7 +351,7 @@ GEM
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
rake (13.3.1)
rdoc (7.0.3)
rdoc (7.1.0)
erb
psych (>= 4.0.0)
tsort
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.saas
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ gem "fizzy-saas", path: "saas"
gem "console1984", bc: "console1984"
gem "audits1984", bc: "audits1984"

# Native push notifications (iOS/Android)
gem "action_push_native"

# Telemetry
gem "rails_structured_logging", bc: "rails-structured-logging"
gem "sentry-ruby"
Expand Down
46 changes: 42 additions & 4 deletions Gemfile.saas.lock
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ PATH
GEM
remote: https://rubygems.org/
specs:
action_push_native (0.3.1)
activejob (>= 8.0)
activerecord (>= 8.0)
googleauth (~> 1.14)
httpx (~> 1.6)
jwt (>= 2)
railties (>= 8.0)
action_text-trix (2.1.16)
railties
activemodel-serializers-xml (1.0.3)
Expand All @@ -194,8 +201,8 @@ GEM
activemodel (>= 7.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 7.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
anyway_config (2.7.2)
ruby-next-core (~> 1.0)
ast (2.4.3)
Expand Down Expand Up @@ -265,6 +272,12 @@ GEM
tzinfo
faker (3.5.3)
i18n (>= 1.8.11, < 2)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
Expand All @@ -280,7 +293,22 @@ GEM
addressable (>= 2.5.0)
globalid (1.3.0)
activesupport (>= 6.1)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-logging-utils (0.2.0)
googleauth (1.16.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.2.1)
http-2 (1.1.1)
httpx (1.7.1)
http-2 (>= 1.0.0)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
Expand Down Expand Up @@ -352,6 +380,9 @@ GEM
mocha (3.0.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
multi_json (1.19.1)
net-http (0.9.1)
uri (>= 0.11.1)
net-http-persistent (4.0.8)
connection_pool (>= 2.2.4, < 4)
net-imap (0.6.2)
Expand Down Expand Up @@ -384,6 +415,7 @@ GEM
nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4)
openssl (4.0.0)
os (1.1.4)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.0)
Expand Down Expand Up @@ -433,7 +465,7 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.2)
puma (7.1.0)
nio4r (~> 2.0)
raabro (1.4.0)
Expand All @@ -460,7 +492,7 @@ GEM
rake-compiler-dock (1.9.1)
rb_sys (0.9.117)
rake-compiler-dock (= 1.9.1)
rdoc (7.0.3)
rdoc (7.1.0)
erb
psych (>= 4.0.0)
tsort
Expand Down Expand Up @@ -523,6 +555,11 @@ GEM
sentry-ruby (6.2.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
sniffer (0.5.0)
anyway_config (>= 1.0)
dry-initializer (~> 3)
Expand Down Expand Up @@ -639,6 +676,7 @@ PLATFORMS
x86_64-linux-musl

DEPENDENCIES
action_push_native
activeresource
audits1984!
autotuner
Expand Down
9 changes: 1 addition & 8 deletions app/helpers/avatars_helper.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
require "zlib"

module AvatarsHelper
AVATAR_COLORS = %w[
#AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
#736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
]

def avatar_background_color(user)
AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
user.avatar_background_color
end

def avatar_tag(user, hidden_for_screen_reader: false, **options)
Expand Down
8 changes: 6 additions & 2 deletions app/javascript/controllers/notifications_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default class extends Controller {

if (registration && subscription) {
this.element.classList.add(this.enabledClass)
} else {
this.subscribeButtonTarget.hidden = false
this.explainerTarget.hidden = true
}
break
}
Expand Down Expand Up @@ -54,8 +57,9 @@ export default class extends Controller {
return navigator.serviceWorker.getRegistration("/service-worker.js", { scope: "/" })
}

#registerServiceWorker() {
return navigator.serviceWorker.register("/service-worker.js", { scope: "/" })
async #registerServiceWorker() {
await navigator.serviceWorker.register("/service-worker.js", { scope: "/" })
return navigator.serviceWorker.ready
}

async #subscribe(registration) {
Expand Down
5 changes: 5 additions & 0 deletions app/jobs/notification/push_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Notification::PushJob < ApplicationJob
def perform(notification)
notification.push
end
end
7 changes: 0 additions & 7 deletions app/jobs/push_notification_job.rb

This file was deleted.

12 changes: 0 additions & 12 deletions app/models/concerns/push_notifiable.rb

This file was deleted.

4 changes: 4 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def description_for(user)
Event::Description.new(self, user)
end

def high_priority_push?
action.card_assigned?
end

private
def dispatch_webhooks
Event::WebhookDispatchJob.perform_later(self)
Expand Down
4 changes: 4 additions & 0 deletions app/models/mention.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def notifiable_target
source
end

def high_priority_push?
true
end

private
def watch_source_by_mentionee
source.watch_by(mentionee)
Expand Down
3 changes: 2 additions & 1 deletion app/models/notification.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Notification < ApplicationRecord
include PushNotifiable
include Notification::Pushable

belongs_to :account, default: -> { user.account }
belongs_to :user
Expand All @@ -18,6 +18,7 @@ class Notification < ApplicationRecord

delegate :notifiable_target, to: :source
delegate :card, to: :source
delegate :identity, to: :user

def self.read_all
all.each { |notification| notification.read }
Expand Down
53 changes: 53 additions & 0 deletions app/models/notification/default_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class Notification::DefaultPayload
attr_reader :notification

delegate :card, to: :notification

def initialize(notification)
@notification = notification
end

def to_h
{ title: title, body: body, url: url }
end

def title
"New notification"
end

def body
"You have a new notification"
end

def url
notifications_url
end

def category
"default"
end

def high_priority?
false
end

def avatar_url
Rails.application.routes.url_helpers.user_avatar_url(notification.creator, **url_options)
end

private
def card_url(card)
Rails.application.routes.url_helpers.card_url(card, **url_options)
end

def notifications_url
Rails.application.routes.url_helpers.notifications_url(**url_options)
end

def url_options
base_options = Rails.application.routes.default_url_options.presence ||
Rails.application.config.action_mailer.default_url_options ||
{}
base_options.merge(script_name: notification.account.slug)
end
end
67 changes: 67 additions & 0 deletions app/models/notification/event_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
class Notification::EventPayload < Notification::DefaultPayload
include ExcerptHelper

def title
case event.action
when "comment_created"
"RE: #{card_title}"
else
card_title
end
end

def body
case event.action
when "comment_created"
format_excerpt(event.eventable.body, length: 200)
when "card_assigned"
"Assigned to you by #{event.creator.name}"
when "card_published"
"Added by #{event.creator.name}"
when "card_closed"
card.closure ? "Moved to Done by #{event.creator.name}" : "Closed by #{event.creator.name}"
when "card_reopened"
"Reopened by #{event.creator.name}"
else
event.creator.name
end
end

def url
case event.action
when "comment_created"
card_url_with_comment_anchor(event.eventable)
else
card_url(card)
end
end

def category
case event.action
when "card_assigned" then "assignment"
when "comment_created" then "comment"
else "card"
end
end

def high_priority?
event.action.card_assigned?
end

private
def event
notification.source
end

def card_title
card.title.presence || "Card #{card.number}"
end

def card_url_with_comment_anchor(comment)
Rails.application.routes.url_helpers.card_url(
comment.card,
anchor: ActionView::RecordIdentifier.dom_id(comment),
**url_options
)
end
end
Loading