Since embedded signup has been disabled on production, WhatsApp inboxes that were created through it have no way to be managed going forward. This PR lets admins transfer an embedded signup inbox to a manual Cloud API setup: the inbox settings Configuration tab now shows the webhook verification token and a "Switch to Manual Setup" form (pre-populated with the Phone Number ID, Business Account ID, and API key stored during the embedded signup journey) instead of the old Reconfigure button. Saving the form updates the channel's `provider_config` without the `source: embedded_signup` marker, so the inbox becomes a regular manually-configured WhatsApp Cloud inbox. The backend re-validates the submitted credentials against Meta before accepting the change. ## How to test 1. Open the settings page of a WhatsApp inbox created via embedded signup → Configuration tab. 2. The Reconfigure button is gone; you see the webhook verification token and a Switch to Manual Setup form pre-filled with the stored credentials. 3. Configure the webhook in your own Meta app using the verification token, enter a permanent access token from that app, and click "Switch to Manual Setup". 4. On success the page switches to the standard manual configuration view (verify token, API key update), and messaging continues to work with the new credentials. Invalid credentials are rejected with an error. ## What changed - `ConfigurationPage.vue`: replaced the embedded-signup Reconfigure section (and the hidden reauthorize component) with the manual transfer form. - New `WHATSAPP_MANUAL_TRANSFER_*` translation keys. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
160 lines
6.0 KiB
Ruby
160 lines
6.0 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: channel_whatsapp
|
|
#
|
|
# id :bigint not null, primary key
|
|
# message_templates :jsonb
|
|
# message_templates_last_updated :datetime
|
|
# phone_number :string not null
|
|
# provider :string default("default")
|
|
# provider_config :jsonb
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_channel_whatsapp_on_phone_number (phone_number) UNIQUE
|
|
#
|
|
|
|
class Channel::Whatsapp < ApplicationRecord
|
|
include Channelable
|
|
include Reauthorizable
|
|
|
|
self.table_name = 'channel_whatsapp'
|
|
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
|
|
|
# default at the moment is 360dialog lets change later.
|
|
PROVIDERS = %w[default whatsapp_cloud].freeze
|
|
before_validation :ensure_webhook_verify_token
|
|
|
|
validates :provider, inclusion: { in: PROVIDERS }
|
|
validates :phone_number, presence: true, uniqueness: true
|
|
validate :validate_provider_config
|
|
|
|
after_create :sync_templates
|
|
after_update_commit :log_credentials_transfer, if: :saved_change_to_provider_config?
|
|
before_destroy :teardown_webhooks
|
|
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
|
|
|
def name
|
|
'Whatsapp'
|
|
end
|
|
|
|
# Mirrors Channel::TwilioSms#voice_enabled? so the call subsystem can duck-type across providers.
|
|
# Meta's Calling API is available to any whatsapp_cloud inbox (embedded-signup or manual keys);
|
|
# only 360dialog (default provider) can't reach the call APIs.
|
|
def voice_enabled?
|
|
voice_calling_supported? &&
|
|
provider_config['calling_enabled'].present? &&
|
|
account.feature_enabled?('channel_voice')
|
|
end
|
|
|
|
# Mutes only the incoming side of calling; default on, so only an explicit false disables inbound.
|
|
def inbound_calls_enabled?
|
|
provider_config['inbound_calls_enabled'] != false
|
|
end
|
|
|
|
# Whether this inbox can do WhatsApp calling at all. Meta's Calling API is
|
|
# reachable by any whatsapp_cloud inbox, so 360dialog inboxes can't be toggled
|
|
# on even though calling_enabled would persist.
|
|
def voice_calling_supported?
|
|
provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def provider_service
|
|
if provider == 'whatsapp_cloud'
|
|
Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self)
|
|
else
|
|
Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self)
|
|
end
|
|
end
|
|
|
|
# Enables voice: turns calling on at Meta (idempotent), then re-registers webhooks
|
|
# with the in-memory calling_enabled flag so the `calls` field is subscribed. The
|
|
# flag is persisted only after registration succeeds, so a webhook failure can't
|
|
# leave the inbox reporting voice_enabled? while the WABA isn't subscribed to calls.
|
|
# Saved with validate: false to skip validate_provider_config's remote credential
|
|
# re-check, which could spuriously fail and desync the flag from Meta.
|
|
def enable_voice_calling!
|
|
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
|
|
raise 'WhatsApp calling requires the channel_voice feature' unless account.feature_enabled?('channel_voice')
|
|
|
|
provider_service.update_calling_status('ENABLED')
|
|
self.provider_config = provider_config.merge('calling_enabled' => true)
|
|
webhook_setup_service.register_callback
|
|
save!(validate: false)
|
|
end
|
|
|
|
# Disables voice: unsets calling_enabled (gates the call subsystem) and re-registers
|
|
# webhooks, which drops `calls` from the subscription (best-effort, so a Meta outage
|
|
# can't trap admins). Leaves Meta's WABA calling.status untouched.
|
|
def disable_voice_calling!
|
|
raise 'WhatsApp calling requires a whatsapp_cloud inbox' unless voice_calling_supported?
|
|
|
|
self.provider_config = provider_config.merge('calling_enabled' => false)
|
|
save!(validate: false)
|
|
begin
|
|
webhook_setup_service.register_callback
|
|
rescue StandardError => e
|
|
Rails.logger.warn "[WHATSAPP CALL] disable webhook re-subscribe failed: #{e.message}"
|
|
end
|
|
end
|
|
|
|
def mark_message_templates_updated
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
update_column(:message_templates_last_updated, Time.zone.now)
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
end
|
|
|
|
delegate :send_message, to: :provider_service
|
|
delegate :send_template, to: :provider_service
|
|
delegate :sync_templates, to: :provider_service
|
|
delegate :media_url, to: :provider_service
|
|
delegate :api_headers, to: :provider_service
|
|
|
|
def setup_webhooks
|
|
perform_webhook_setup
|
|
rescue StandardError => e
|
|
Rails.logger.error "[WHATSAPP] Webhook setup failed: #{e.message}"
|
|
prompt_reauthorization!
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_webhook_verify_token
|
|
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud'
|
|
end
|
|
|
|
def validate_provider_config
|
|
errors.add(:provider_config, 'Invalid Credentials') unless provider_service.validate_provider_config?
|
|
end
|
|
|
|
# Logs only credential changes, so config-only saves (e.g. calling toggles) stay silent.
|
|
def log_credentials_transfer
|
|
before, after = saved_change_to_provider_config
|
|
keys = %w[api_key phone_number_id business_account_id]
|
|
return if before.nil? || before.values_at(*keys) == after.values_at(*keys)
|
|
|
|
Rails.logger.info("[WHATSAPP_MANUAL_TRANSFER] success account_id=#{account_id} channel_id=#{id}")
|
|
end
|
|
|
|
def perform_webhook_setup
|
|
webhook_setup_service.perform
|
|
end
|
|
|
|
def webhook_setup_service
|
|
Whatsapp::WebhookSetupService.new(self, provider_config['business_account_id'], provider_config['api_key'])
|
|
end
|
|
|
|
def teardown_webhooks
|
|
Whatsapp::WebhookTeardownService.new(self).perform
|
|
end
|
|
|
|
def should_auto_setup_webhooks?
|
|
# Only auto-setup webhooks for whatsapp_cloud provider with manual setup
|
|
# Embedded signup calls setup_webhooks explicitly in EmbeddedSignupService
|
|
provider == 'whatsapp_cloud' && provider_config['source'] != 'embedded_signup'
|
|
end
|
|
end
|