Add push notifications scaffolding via noticed v2#59
Merged
Conversation
PR #1 of 5 in #58. Scaffolds the Rails-side groundwork for native push notifications. Provider integration (APNs + FCM) and ItemTag AASM wiring follow in PR #2 once APNs .p8 and FCM service-account JSON are provisioned. Client work (free + paid iOS/Android) follows in PRs #3-5. What lands here - noticed v2 gem + the two engine migrations (Noticed::Event, Noticed::Notification, both UUID-keyed to match this substrate's primary_key_type) - Device model + migration: shopkeeper-scoped, unique on [platform, token], last_active_at for staleness scoping; ios/android enum - Api::V1::Shopkeeper::DevicesController: POST /api/v1/shopkeeper/devices — idempotent upsert (rebinds token to current_shopkeeper if it previously belonged to someone else, e.g. shared device after sign-out/sign-in); 201 on create, 200 on touch DELETE /api/v1/shopkeeper/devices/:id — unregister (404 on someone else's device, scoped via current_shopkeeper.devices) - DevicePolicy + DeviceSerializer following existing substrate conventions (BasePolicy + JSONAPI::Serializer) - ApplicationNotifier base + example ItemTagCalledNotifier (no delivery methods wired yet — title/body/url are i18n-resolved via notification_methods so PR #2 just needs to add deliver_by :ios + :android and trigger from ItemTag's AASM complete event) - Shopkeeper.has_many :devices (dependent: :destroy) and :notifications (as: :recipient, class: Noticed::Notification) - Locale entries under notifiers.item_tag_called Tests: 21 new runs (Device model 9, DevicesController 8, notifier 4), 0 failures. Full suite now 419 runs / 868 assertions / 0 failures / 0 errors / 0 skips. rubocop clean (239 files, 0 offenses).
Install action_push_native 0.3.x and generate ApplicationPushNotification, ApplicationPushDevice, ApplicationPushNotificationJob, and config/push.yml. Add deliver_by :action_push_native to ItemTagCalledNotifier so push notifications route through Rails 8.1's Action Push Native (single abstraction over APNs + FCM) instead of the Noticed gem's per-platform :ios / :fcm deliverers. APNs/FCM credentials remain placeholders in config/push.yml — provision via bin/rails credentials:edit before enabling delivery. Bridging the existing Device registration API to ApplicationPushDevice (so registered tokens actually flow into Action Push Native delivery) is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substrate post-Phase-1 (#45) is generic single-resource CRUD, not queue-only — `ItemTag.name` can be a queue number ("A001"), a pet name ("Mittens"), a task title, etc. The copy "Number %{number} is up" only reads correctly for the queue case, and the agent's renamer doesn't substitute the word "Number" (it's not in the rename plan), so the wrong copy ships to every renamed app. Change to "%{name} is ready" — generalizes cleanly across the queue, reservation, vet-clinic, and task-tracker domains the substrate targets. Body unchanged. Test passes; rubocop clean.
Reverses the previous "Number → name" / "is ready" decision once it became clear that any state-verb baked into the substrate's notifier title or class name fights the agent's domain-adapt step. The agent extends/renames the AASM state machine per spec (idled/completed → e.g. waiting/seated for restaurant, pending/seen for vet clinic), but its rename plan only handles the four model-level tokens (Shop / Shopkeeper / ItemTag / NativeAppTemplate). State names cascading into notifier file/class/locale-key/title are out of scope for the rename-safety contract (#57). So the substrate's notifier ships state-verb-free: - File: item_tag_called_notifier.rb → item_tag_notifier.rb - Class: ItemTagCalledNotifier → ItemTagNotifier - Locale key: notifiers.item_tag_called → notifiers.item_tag - Title: "%{name} is ready" → "%{name}" - Body: "Please proceed to %{shop}." → "%{shop}" `ItemTag` itself IS in the rename plan, so file/class/locale-key cascade through `item_tag → patient/reservation/todo` cleanly. `%{name}` and `%{shop}` are interpolation keys, not renameable tokens. Result: substrate copy survives any state-verb rewrite the agent's adapt step does, at the cost of vague substrate copy. The adapt step can rewrite richer per-domain copy when it wants. Tests + rubocop clean.
Push-notification UX convention is source-in-title, event-in-body —
WhatsApp (sender → message), Slack (channel → message), Calendar
(event → location). Shop is the recognizable persistent entity that
anchors the notification; item name is variable per-event content.
Title: %{name} → %{shop}
Body: %{shop} → %{name}
Tests + rubocop clean.
config/push.yml looks up Rails.application.credentials.dig( :action_push_native, :apns, :key_id) and friends, but the credentials template that seeds `bin/rails credentials:edit` on first generation didn't expose those keys. Fresh developers would hit silent nil on first push delivery without knowing where the lookup expected the secret. Adds the same shape Resend's api_key already follows: empty placeholder under the documented key path. Comment notes which inputs are needed (APNs key_id + .p8 contents, FCM service-account JSON).
Three deployment-specific values were still hard-coded as placeholders in config/push.yml: - apple.team_id (Apple Developer team identifier — per-deployer) - apple.topic (iOS bundle identifier — per-deployment) - google.project_id (Firebase project identifier — per-deployment) These don't belong in source. apple.topic in particular is a rename- pipeline trap: the agent renames the iOS bundle id when generating a domain-customized variant (com.nativeapptemplate.* → com.<spec>.*), but the rename pipeline only operates on code/locales/OpenAPI — not on push.yml strings. So a hard-coded `your.bundle.identifier` here silently desyncs from the renamed app's actual bundle id and push delivery breaks with a non-obvious error. Move all three to Rails.application.credentials.dig(:action_push_native, ...) so they're deploy-time configuration, not source-controlled state. Add the same fields to the credentials.yml.tt template so `bin/rails credentials:edit` exposes the expected key paths. Tests + rubocop clean.
Layer 1 of the agent's reviewer (per the agent's docs/SPEC.md) checks OpenAPI parity between Rails ↔ iOS networking ↔ Android repository layers. Adding the Device controller without the corresponding spec entries means PRs #3-5 (the iOS/Android push registration clients) wouldn't have a contract to integrate against and would fail Layer 1 contract-parity scan. Adds: - Tag: Devices - Path POST /devices: idempotent register; 201 on create, 200 on touch, 422 on validation error - Path DELETE /devices/{deviceId}: 204 no_content, 404 if device isn't owned by current_shopkeeper - Schemas: DeviceAttributes, Device, DeviceCreateRequest (jsonapi-style envelope to match the rest of the API) YAML parses; paths now 25, schemas now 38.
This was referenced May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #1 of 5 in #58 — scaffolds the Rails-side groundwork for native push notifications.
This PR is provider-credential-agnostic: APNs
.p8+ FCM service-account JSON aren't required to merge or run tests. Provider integration + ItemTag AASM wiring lands in PR #2 once credentials are provisioned. Client work (free + paid iOS/Android) follows in PRs #3–#5.What lands
noticedv2.x gem + the two engine migrations (Noticed::Event,Noticed::Notification, both UUID-keyed to match this substrate'sprimary_key_type :uuid).Devicemodel + migration — shopkeeper-scoped, unique on[platform, token],last_active_atfor staleness scoping;ios/androidenum.Api::V1::Shopkeeper::DevicesController:POST /api/v1/shopkeeper/devices— idempotent upsert (rebinds token tocurrent_shopkeeperif it previously belonged to someone else, e.g. shared device after sign-out/sign-in); 201 on create, 200 on touch.DELETE /api/v1/shopkeeper/devices/:id— unregister (404 on someone else's device, scoped viacurrent_shopkeeper.devices).DevicePolicy+DeviceSerializerfollowing existing substrate conventions (Api::Shopkeeper::BasePolicy+JSONAPI::Serializer).ApplicationNotifierbase + exampleItemTagCalledNotifier— title/body/url i18n-resolved vianotification_methods. No delivery methods wired yet; PR test4 #2 addsdeliver_by :ios+:androidand triggers fromItemTag's AASMcompleteevent.Shopkeepergainshas_many :devices(dependent: :destroy) andhas_many :notifications, as: :recipient, class_name: 'Noticed::Notification'.notifiers.item_tag_called.Test plan
bin/rails test— 419 runs, 868 assertions, 0 failures, 0 errors, 0 skips (was 398/815 before; +21 new runs / +53 assertions, all green).bin/rubocop— 239 files inspected, no offenses.bin/rails db:migrate— all 3 new migrations apply cleanly (UUID primary keys, indexes, FK)./deviceswith same(platform, token)doesn't duplicate; toucheslast_active_atinstead.current_shopkeeper.devices.findrather than pundit-then-find; matchesRecordNotFoundrescue contract elsewhere in the controllers).Known follow-ups (not this PR)
apnotic+googleauthdeps +deliver_by :ios+:androidconfig + AASMcompletetrigger onItemTag+ integration test (mocked APNs/FCM). Blocked on credentials.🤖 Generated with Claude Code