Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
class Notification::EmailNotificationService
pattr_initialize [:notification!]
def perform
# don't send emails if user read the push notification already
return if notification.read_at.present?
# don't send emails if user is not confirmed
return if notification.user.confirmed_at.nil?
return unless user_subscribed_to_notification?
# TODO : Clean up whatever happening over here
# Segregate the mailers properly
AgentNotifications::ConversationNotificationsMailer.with(account: notification.account).public_send(notification
.notification_type.to_s, notification.primary_actor, notification.user, notification.secondary_actor).deliver_later
end
private
def user_subscribed_to_notification?
notification_setting = notification.user.notification_settings.find_by(account_id: notification.account.id)
return true if notification_setting.public_send("email_#{notification.notification_type}?")
false
end
end

View File

@@ -0,0 +1,40 @@
class Notification::FcmService
SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'].freeze
def initialize(project_id, credentials)
@project_id = project_id
@credentials = credentials
@token_info = nil
end
def fcm_client
FCM.new(current_token, credentials_path, @project_id)
end
private
def current_token
@token_info = generate_token if @token_info.nil? || token_expired?
@token_info[:token]
end
def token_expired?
Time.zone.now >= @token_info[:expires_at]
end
def generate_token
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: credentials_path,
scope: SCOPES
)
token = authorizer.fetch_access_token!
{
token: token['access_token'],
expires_at: Time.zone.now + token['expires_in'].to_i
}
end
def credentials_path
StringIO.new(@credentials)
end
end

View File

@@ -0,0 +1,172 @@
class Notification::PushNotificationService
include Rails.application.routes.url_helpers
pattr_initialize [:notification!]
def perform
return unless user_subscribed_to_notification?
notification_subscriptions.each do |subscription|
send_browser_push(subscription)
send_fcm_push(subscription)
send_push_via_chatwoot_hub(subscription)
end
end
private
delegate :user, to: :notification
delegate :notification_subscriptions, to: :user
delegate :notification_settings, to: :user
def user_subscribed_to_notification?
notification_setting = notification_settings.find_by(account_id: notification.account.id)
return true if notification_setting.public_send("push_#{notification.notification_type}?")
false
end
def conversation
@conversation ||= notification.conversation
end
def push_message
{
title: notification.push_message_title,
tag: "#{notification.notification_type}_#{conversation.display_id}_#{notification.id}",
url: push_url
}
end
def push_url
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id)
end
def can_send_browser_push?(subscription)
VapidService.public_key && subscription.browser_push?
end
def browser_push_payload(subscription)
{
message: JSON.generate(push_message),
endpoint: subscription.subscription_attributes['endpoint'],
p256dh: subscription.subscription_attributes['p256dh'],
auth: subscription.subscription_attributes['auth'],
vapid: {
subject: push_url,
public_key: VapidService.public_key,
private_key: VapidService.private_key
},
ssl_timeout: 5,
open_timeout: 5,
read_timeout: 5
}
end
def send_browser_push(subscription)
return unless can_send_browser_push?(subscription)
WebPush.payload_send(**browser_push_payload(subscription))
Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}")
rescue StandardError => e
handle_browser_push_error(e, subscription)
end
def handle_browser_push_error(error, subscription)
case error
when WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized
Rails.logger.info "WebPush subscription expired: #{error.message}"
subscription.destroy!
when WebPush::TooManyRequests
Rails.logger.warn "WebPush rate limited for #{user.email} on account #{notification.account.id}: #{error.message}"
when Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout
Rails.logger.error "WebPush operation error: #{error.message}"
else
ChatwootExceptionTracker.new(error, account: notification.account).capture_exception
true
end
end
def send_fcm_push(subscription)
return unless firebase_credentials_present?
return unless subscription.fcm?
fcm_service = Notification::FcmService.new(
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil), GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
)
fcm = fcm_service.fcm_client
response = fcm.send_v1(fcm_options(subscription))
remove_subscription_if_error(subscription, response)
end
def send_push_via_chatwoot_hub(subscription)
return if firebase_credentials_present?
return unless chatwoot_hub_enabled?
return unless subscription.fcm?
ChatwootHub.send_push(fcm_options(subscription))
end
def firebase_credentials_present?
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil) && GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
end
def chatwoot_hub_enabled?
ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
end
def remove_subscription_if_error(subscription, response)
if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
subscription.destroy!
else
Rails.logger.info("FCM push sent to #{user.email} with title #{push_message[:title]}")
end
end
def fcm_options(subscription)
{
'token': subscription.subscription_attributes['push_token'],
'data': fcm_data,
'notification': fcm_notification,
'android': fcm_android_options,
'apns': fcm_apns_options,
'fcm_options': {
analytics_label: 'Label'
}
}
end
def fcm_data
{
payload: {
data: {
notification: notification.fcm_push_data
}
}.to_json
}
end
def fcm_notification
{
title: notification.push_message_title,
body: notification.push_message_body
}
end
def fcm_android_options
{
priority: 'high'
}
end
def fcm_apns_options
{
payload: {
aps: {
sound: 'default',
category: Time.zone.now.to_i.to_s
}
}
}
end
end