Fix database issues and add APNS configuration
- 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
This commit is contained in:
committed by
Rosa Gutierrez
parent
3c54cd84fc
commit
55336873b2
@@ -2,13 +2,31 @@
|
||||
|
||||
PORT=3006
|
||||
USE_TAILSCALE=0
|
||||
USE_APNS=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--tailscale) USE_TAILSCALE=1 ;;
|
||||
--apns) USE_APNS=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$USE_APNS" = "1" ]; then
|
||||
if [ ! -f tmp/saas.txt ]; then
|
||||
echo "Enabling SaaS mode for APNs..."
|
||||
./bin/rails saas:enable
|
||||
fi
|
||||
echo "Loading APNs credentials from 1Password..."
|
||||
if ! eval "$(BUNDLE_GEMFILE=Gemfile.saas bundle exec apns-dev)"; then
|
||||
echo "Error: failed to load APNs credentials. Are you signed into 1Password?" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$APNS_ENCRYPTION_KEY" ] || [ -z "$APNS_KEY_ID" ]; then
|
||||
echo "Error: APNs credentials not set. Missing APNS_ENCRYPTION_KEY or APNS_KEY_ID." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f tmp/solid-queue.txt ]; then
|
||||
export SOLID_QUEUE_IN_PUMA=false
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<% if Fizzy.saas? %>
|
||||
<%= ERB.new(File.read(Rails.root.join("saas/config/push.yml"))).result %>
|
||||
<% else %>
|
||||
shared:
|
||||
apple: {}
|
||||
google: {}
|
||||
<% end %>
|
||||
@@ -5,7 +5,7 @@ class CreateActionPushNativeDevices < ActiveRecord::Migration[8.0]
|
||||
t.string :name
|
||||
t.string :platform, null: false
|
||||
t.string :token, null: false
|
||||
t.belongs_to :owner, polymorphic: true
|
||||
t.belongs_to :owner, polymorphic: true, type: :uuid
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
Generated
+13
@@ -69,6 +69,19 @@ ActiveRecord::Schema[8.2].define(version: 2026_02_18_120000) do
|
||||
t.index ["external_account_id"], name: "index_accounts_on_external_account_id", unique: true
|
||||
end
|
||||
|
||||
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name"
|
||||
t.uuid "owner_id"
|
||||
t.string "owner_type"
|
||||
t.string "platform", null: false
|
||||
t.string "token", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "uuid", null: false
|
||||
t.index ["owner_type", "owner_id", "uuid"], name: "idx_on_owner_type_owner_id_uuid_a42e3920d5", unique: true
|
||||
t.index ["owner_type", "owner_id"], name: "index_action_push_native_devices_on_owner"
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.text "body", size: :long
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET Beta/APNS_ENCRYPTION_KEY Beta/APNS_KEY_ID Beta/APNS_TEAM_ID Beta/APNS_TOPIC)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
|
||||
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
|
||||
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
|
||||
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
|
||||
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
|
||||
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
|
||||
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
|
||||
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET Production/APNS_ENCRYPTION_KEY Production/APNS_KEY_ID Production/APNS_TEAM_ID Production/APNS_TOPIC)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
|
||||
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
|
||||
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
|
||||
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
|
||||
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
|
||||
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
|
||||
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
|
||||
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET)
|
||||
SECRETS=$(kamal secrets fetch --adapter 1password --account 23QPQDKZC5BKBIIG7UGT5GR5RM --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET Staging/APNS_ENCRYPTION_KEY Staging/APNS_KEY_ID Staging/APNS_TEAM_ID Staging/APNS_TOPIC)
|
||||
|
||||
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
|
||||
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
|
||||
@@ -25,3 +25,7 @@ STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $S
|
||||
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
|
||||
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
|
||||
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
|
||||
APNS_ENCRYPTION_KEY=$(kamal secrets extract APNS_ENCRYPTION_KEY $SECRETS)
|
||||
APNS_KEY_ID=$(kamal secrets extract APNS_KEY_ID $SECRETS)
|
||||
APNS_TEAM_ID=$(kamal secrets extract APNS_TEAM_ID $SECRETS)
|
||||
APNS_TOPIC=$(kamal secrets extract APNS_TOPIC $SECRETS)
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
class Users::DevicesController < ApplicationController
|
||||
before_action :set_devices
|
||||
|
||||
def index
|
||||
@devices = Current.user.devices.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def create
|
||||
attrs = device_params
|
||||
device = Current.user.devices.find_or_create_by(uuid: attrs[:uuid])
|
||||
device.update!(token: attrs[:token], name: attrs[:name], platform: attrs[:platform])
|
||||
device = @devices.find_or_initialize_by(uuid: params.require(:uuid))
|
||||
device.update!(device_params)
|
||||
head :created
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
head :unprocessable_entity
|
||||
rescue ArgumentError
|
||||
head :bad_request
|
||||
end
|
||||
|
||||
def destroy
|
||||
Current.user.devices.find_by(id: params[:id])&.destroy
|
||||
@devices.destroy_by(id: params[:id])
|
||||
redirect_to users_devices_path, notice: "Device removed"
|
||||
end
|
||||
|
||||
private
|
||||
def set_devices
|
||||
@devices = Current.user.devices.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def device_params
|
||||
params.permit(:uuid, :token, :platform, :name).tap do |p|
|
||||
p[:platform] = p[:platform].to_s.downcase
|
||||
raise ActionController::BadRequest unless p[:platform].in?(%w[apple google])
|
||||
raise ActionController::BadRequest if p[:uuid].blank?
|
||||
raise ActionController::BadRequest if p[:token].blank?
|
||||
params.permit(:token, :platform, :name).tap do |permitted|
|
||||
permitted[:platform] = permitted[:platform].to_s.downcase if permitted[:platform].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class ApplicationPushNotification < ActionPushNative::Notification
|
||||
queue_as :default
|
||||
self.enabled = !Rails.env.local?
|
||||
self.enabled = Fizzy.saas? && (!Rails.env.local? || ENV["ENABLE_NATIVE_PUSH"] == "true")
|
||||
end
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
shared:
|
||||
apple:
|
||||
key_id: <%= ENV["APNS_KEY_ID"] || Rails.application.credentials.dig(:action_push_native, :apns, :key_id) %>
|
||||
encryption_key: <%= (ENV["APNS_ENCRYPTION_KEY"] || Rails.application.credentials.dig(:action_push_native, :apns, :key))&.dump %>
|
||||
team_id: YOUR_TEAM_ID # Your 10-character Apple Developer Team ID (not secret)
|
||||
topic: com.yourcompany.fizzy # Your app's bundle identifier (not secret)
|
||||
# Uncomment for local development with Xcode builds (uses APNs sandbox):
|
||||
# connect_to_development_server: true
|
||||
encryption_key: <%= (ENV["APNS_ENCRYPTION_KEY"]&.gsub("\\n", "\n") || Rails.application.credentials.dig(:action_push_native, :apns, :key))&.dump %>
|
||||
team_id: <%= ENV["APNS_TEAM_ID"] || Rails.application.credentials.dig(:action_push_native, :apns, :team_id) || "2WNYUYRS7G" %>
|
||||
topic: <%= ENV["APNS_TOPIC"] || Rails.application.credentials.dig(:action_push_native, :apns, :topic) || "do.fizzy.app.ios" %>
|
||||
connect_to_development_server: <%= Rails.env.local? %>
|
||||
google:
|
||||
encryption_key: <%= (ENV["FCM_ENCRYPTION_KEY"] || Rails.application.credentials.dig(:action_push_native, :fcm, :key))&.dump %>
|
||||
project_id: your-firebase-project # Your Firebase project ID (not secret)
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# Fetches APNs development environment variables from 1Password.
|
||||
#
|
||||
# Usage: eval "$(bundle exec apns-dev)"
|
||||
|
||||
OP_ACCOUNT = "23QPQDKZC5BKBIIG7UGT5GR5RM"
|
||||
OP_VAULT = "Mobile"
|
||||
OP_ITEM = "37signals Push Notifications key"
|
||||
|
||||
def op_read(field)
|
||||
`op read "op://#{OP_VAULT}/#{OP_ITEM}/#{field}" --account #{OP_ACCOUNT} 2>/dev/null`.strip
|
||||
end
|
||||
|
||||
key_id = op_read("key ID")
|
||||
team_id = op_read("team ID")
|
||||
encryption_key = op_read("AuthKey_3CR5J2W8Q6.p8")
|
||||
|
||||
if key_id.empty? || encryption_key.empty?
|
||||
warn "Error: Could not fetch APNs credentials from 1Password"
|
||||
warn "Make sure you're signed in: op signin --account #{OP_ACCOUNT}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts %Q(export APNS_KEY_ID="#{key_id}")
|
||||
puts %Q(export APNS_TEAM_ID="#{team_id}")
|
||||
puts %Q(export APNS_ENCRYPTION_KEY="#{encryption_key.gsub("\n", "\\n")}")
|
||||
puts %Q(export APNS_TOPIC="do.fizzy.app.ios")
|
||||
puts %Q(export ENABLE_NATIVE_PUSH="true")
|
||||
|
||||
warn ""
|
||||
warn "APNs credentials loaded for development"
|
||||
warn " Key ID: #{key_id}"
|
||||
warn " Team ID: #{team_id}"
|
||||
warn " Topic: do.fizzy.app.ios"
|
||||
warn " Native push: enabled"
|
||||
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
||||
end
|
||||
|
||||
spec.bindir = "exe"
|
||||
spec.executables = [ "stripe-dev" ]
|
||||
spec.executables = [ "apns-dev", "stripe-dev" ]
|
||||
|
||||
spec.add_dependency "rails", ">= 8.1.0.beta1"
|
||||
spec.add_dependency "queenbee"
|
||||
|
||||
@@ -23,8 +23,8 @@ module Fizzy
|
||||
headers: app.config.public_file_server.headers
|
||||
end
|
||||
|
||||
initializer "fizzy_saas.push_config", before: "action_push_native.config" do |app|
|
||||
app.paths.add "config/push", with: root.join("config/push.yml")
|
||||
initializer "fizzy_saas.push_config", after: "action_push_native.config" do |app|
|
||||
app.paths["config/push"].unshift(root.join("config/push.yml").to_s)
|
||||
end
|
||||
|
||||
initializer "fizzy.saas.routes", after: :add_routing_paths do |app|
|
||||
@@ -157,7 +157,7 @@ module Fizzy
|
||||
::Account.include Account::Billing, Account::Limited
|
||||
::User.include User::NotifiesAccountOfEmailChange
|
||||
::User.include User::Devices
|
||||
::NotificationPusher.include NotificationPusher::Native
|
||||
::NotificationPusher.prepend NotificationPusher::Native
|
||||
::Signup.prepend Fizzy::Saas::Signup
|
||||
CardsController.include(Card::LimitedCreation)
|
||||
Cards::PublishesController.include(Card::LimitedPublishing)
|
||||
|
||||
Reference in New Issue
Block a user