The manage devices link used `devices_path` but the route is defined in
the saas engine, so it needs `saas.devices_path`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include the shortened familiar name format (e.g., "Salvador D.")
for display in iOS notification titles.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move avatar_background_color logic from helper to User::Avatar concern
so it can be accessed from models. Include creator_id, creator_initials,
and creator_avatar_color in native push notifications for local avatar
rendering on iOS.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Each target now implements process directly with its own logic,
rather than using processable?/perform_push hooks. The pushable?
check is done once in Notification#push before iterating targets.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace separate WebPushJob and NativePushJob with a single PushJob
that calls notification.push, which iterates over registered targets.
Each target handles its own delivery - Web pushes synchronously via
the pool, Native enqueues device-level jobs via deliver_later_to.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use polymorphism instead of case statements in Native push target:
- DefaultPayload#category returns "default", #high_priority? returns false
- EventPayload#category returns "assignment"/"comment"/"card" based on action
- MentionPayload#category returns "mention", #high_priority? returns true
This simplifies the Native push target by delegating source-specific
logic to the appropriate payload classes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add disallow_account_scope to skip tenant requirement
- Move routes to saas/config/routes.rb (engine routes)
- Use saas.devices_path/saas.device_path for engine route helpers
- Update tests to work without tenant context
Devices belong to Identity (global), not Account, so they don't
need tenant context in the URL.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a session is destroyed (user logs out), all devices registered
to that session are automatically destroyed, preventing push
notifications from being sent to logged-out devices.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PushTarget#push reads more naturally than Push#push. The push target
is the thing that pushes notifications to a specific destination.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The notification now owns its payload via #payload method in Pushable,
allowing direct access like notification.payload.title. Push classes
simply use the notification's payload rather than building it themselves.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace NotificationPusher with a cleaner architecture:
- Add Notification::Pushable concern with push target registry
- Add Notification::Push base class with template methods
- Add Notification::Push::Web for web push (OSS)
- Add Notification::Push::Native for native push (SaaS)
- Add Notification::WebPushJob and Notification::NativePushJob
Key design:
- Registry pattern: Notification.register_push_target(:web)
- Template method: push calls should_push? then perform_push
- Subclasses override should_push? (with super) and perform_push
- Each target handles its own job enqueueing
Also:
- Add Notification#pushable? for checking push eligibility
- Add Notification#identity delegation to user
- Reorganize tests to match new class structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tidy up saas engine a bit more
Devices now belong to Identity instead of User, allowing a single
device registration to work across all accounts an identity has
access to.
- Move User::Devices to Identity::Devices
- Update DevicesController to use Current.identity
- Update NotificationPusher::Native to use user.identity.devices
- Clean up tests to use @identity directly
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix reference to `user.devices`, left-over from the identity switch
- Use RESTful DELETE /devices/:id where :id can be token or database ID
- Remove redundant unregister collection route
- Remove old Users::DevicesController
- Return 404 when device not found instead of silently succeeding
- Return 422 for invalid platform via ActiveRecord validation
- Update action_push_native to main branch (includes validate: true on enum)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove Users namespace from DevicesController (now just DevicesController)
- Create ApplicationPushDevice model extending ActionPushNative::Device
- Move device registration logic (find_or_initialize + update) to model
- Update User::Devices concern to use ApplicationPushDevice
- Fix push notification tests (endpoint validation, job count expectations)
- Update push_config_test to use ActionPushNative.config
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove UUID column from devices table entirely
- Update unique index from (owner, uuid) to (owner, token)
- Simplify create action to just create device records
- Add token-based unregister route for API clients
- Consolidate error handling with rescue_from
- Update fixtures to remove uuid references
Devices are now identified by (owner, token) instead of UUID.
This simplifies the client-side registration flow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix owner_id type to UUID in devices migration
- Fix NOT NULL crash in device registration
- Add APNS config and 1Password integration
- Add --apns flag to bin/dev for local development
- Clean up devices controller
- Rename SaasAdminController to Admin::AuditController
- Override require_authentication to support bearer tokens alongside
session auth, allowing API access to audit console
- Add migration for audits1984_auditor_tokens table
- Add controller tests for authentication scenarios including staff
validation on token-based access
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Sync owner email to Stripe when changed
When an account owner changes their email address, update the
corresponding Stripe customer record via a background job.
Also handles ownership transfers: when a user becomes the account
owner, their email is synced to Stripe.
Responsibility chain:
- User::NotifiesAccountOfEmailChange triggers on owner identity change
or when a user becomes owner
- Account::Billing#owner_email_changed enqueues sync job
- Account::SyncStripeCustomerEmailJob performs the update with
polynomial backoff retries
- Account::Subscription#sync_customer_email_to_stripe calls Stripe API
* Address PR feedback: error handling and test coverage
- Handle Stripe::InvalidRequestError in sync_customer_email_to_stripe
(mirrors cancel method behavior for deleted customers)
- Add test for deactivated owner (owner with nil identity)
- Add test for deleted Stripe customer scenario
Shows total, 7-day, and 24-hour counts of paid accounts based on
subscription created_at. Excludes comped accounts and avoids
double-counting accounts with multiple subscriptions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add self-service account deletion
* Disable access to cancelled accounts & implement Stripe interactions
* Add tests
* Fix failing tests
* Remove cancelled accounts from lists
* Fix incorrect redirect
* Use _path instead of _url for consistency
* Don't track how far the inicieration job got
We still want the step tracking so that the job can be interrupted. But, since the scope is idempotent, we don't need to track how far it got.
* Specify the exact time when the data will be deleted
* Fix crash due to unadvancable cursor
* Rename up_for_incineration to due_for_incineration
* Regenrate the schema
* Fix incorrect path check
* Migrate the SQLite schema
* Only show the cancel button on cancellable accounts
* Check that a subscirption method exists before calling it
* Ignore job failures due to missing records when an account gets deleted
* Skip sending notifications on cancelled accounts
* Use collbacks to integrate
* Add a blank line between queue_as and discard_on
* Break checks into methods
* Inline methods
* Rename Account::IncinerateJob
* Run migrations
* Migrate SQLite