command_tower 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/deserializers/command_tower/deserializers/messaging/preferences/update_deserializer.rb +10 -1
- data/app/errors/command_tower/errors/account/push_test_destination_error.rb +21 -0
- data/app/models/command_tower/messaging/inbox_item.rb +6 -0
- data/app/serializers/command_tower/serializers/messaging/inbox.rb +1 -1
- data/app/serializers/command_tower/serializers/messaging/preferences/notification_serializer.rb +2 -0
- data/app/services/command_tower/messaging/execution/adapter_result.rb +11 -1
- data/app/services/command_tower/messaging/execution/adapters/expo/adapter.rb +146 -25
- data/app/services/command_tower/messaging/execution/adapters/expo/configuration.rb +5 -0
- data/app/services/command_tower/messaging/execution/adapters/expo/endpoint_ticket_history.rb +86 -0
- data/app/services/command_tower/messaging/execution/adapters/expo/interruption_level.rb +41 -0
- data/app/services/command_tower/messaging/execution/attempt_ledger.rb +3 -0
- data/app/services/command_tower/messaging/execution/expo_receipt_collection.rb +224 -0
- data/app/services/command_tower/messaging/execution/operation_logger.rb +55 -5
- data/app/services/command_tower/messaging/execution/recovery.rb +8 -0
- data/app/services/command_tower/messaging/inbox/reader.rb +15 -2
- data/app/services/command_tower/messaging/inbox/unread_count_result.rb +3 -2
- data/app/services/command_tower/messaging/notification_types/declaration.rb +19 -1
- data/app/services/command_tower/messaging/notification_types/declaration_validator.rb +62 -0
- data/app/services/command_tower/messaging/notification_types/permitted_choice.rb +55 -0
- data/app/services/command_tower/messaging/notification_types/registry.rb +25 -0
- data/app/services/command_tower/messaging/notification_types.rb +3 -0
- data/app/services/command_tower/messaging/preferences/catalog.rb +7 -0
- data/app/services/command_tower/messaging/preferences/catalog_notification_result.rb +26 -1
- data/app/services/command_tower/messaging/preferences/effective_choice.rb +92 -0
- data/app/services/command_tower/messaging/preferences/preference_state.rb +24 -4
- data/app/services/command_tower/messaging/preferences/store.rb +10 -0
- data/app/services/command_tower/messaging/preferences/update.rb +36 -4
- data/app/services/command_tower/messaging/rendering/channel_renderer.rb +63 -3
- data/app/services/command_tower/messaging/rendering/inbox_document.rb +27 -0
- data/app/services/command_tower/messaging/rendering/push_presentation.rb +90 -0
- data/app/services/command_tower/messaging/rendering/rendered_push_payload.rb +54 -6
- data/app/services/command_tower/messaging/sound_catalog.rb +85 -0
- data/app/services/command_tower/services/account/push/send_self_test.rb +7 -1
- data/app/services/command_tower/services/messaging/communications/produce.rb +27 -0
- data/app/services/command_tower/services/messaging/inbox.rb +5 -1
- data/app/workflows/command_tower/workflows/me/error_mapping.rb +1 -0
- data/app/workflows/command_tower/workflows/messaging/execution/deliver_workflow.rb +5 -1
- data/db/migrate/20261012000001_add_provider_metadata_to_messaging_delivery_attempts.rb +7 -0
- data/db/migrate/20261012000002_add_finished_at_index_to_messaging_delivery_attempts.rb +8 -0
- data/docs/upgrades/0.19.0.md +40 -0
- data/docs/upgrades/README.md +1 -0
- data/lib/command_tower/configuration/messaging/config.rb +10 -0
- data/lib/command_tower/configuration/messaging/expo.rb +61 -0
- data/lib/command_tower/install/baseline.rb +2 -0
- data/lib/command_tower/version.rb +1 -1
- metadata +13 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommandTower
|
|
4
|
+
module Messaging
|
|
5
|
+
# Registered push sounds. The engine owns System Default, a logical id that needs no
|
|
6
|
+
# bundled native asset. Hosts append custom entries through
|
|
7
|
+
# `config.messaging.sounds` (`{ id:, label:, asset: }`); `asset` is the native file
|
|
8
|
+
# reference. Whether the file is physically bundled is verified later (IOS), not here.
|
|
9
|
+
module SoundCatalog
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
ID_FORMAT = /\A[a-z][a-z0-9_]*\z/
|
|
13
|
+
# iOS plays a custom push sound only for a bundled file name with one of these extensions.
|
|
14
|
+
ASSET_EXTENSIONS = %w[.wav .aiff .caf].freeze
|
|
15
|
+
SYSTEM_DEFAULT = { id: "default", label: "System Default", asset: nil }.freeze
|
|
16
|
+
|
|
17
|
+
def entries
|
|
18
|
+
[SYSTEM_DEFAULT] + host_entries.grep(Hash).map { |entry| entry.transform_keys(&:to_sym).freeze }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ids
|
|
22
|
+
entries.pluck(:id)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch(id)
|
|
26
|
+
entries.find { |entry| entry[:id] == id.to_s }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Messages for an invalid host catalog; empty when valid. Registry seal raises on them.
|
|
30
|
+
def validation_errors
|
|
31
|
+
errors = []
|
|
32
|
+
seen = [SYSTEM_DEFAULT[:id]]
|
|
33
|
+
|
|
34
|
+
host_entries.each_with_index do |entry, index|
|
|
35
|
+
label = "sounds[#{index}]"
|
|
36
|
+
unless entry.is_a?(Hash)
|
|
37
|
+
errors << "#{label} must be a hash"
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
entry = entry.transform_keys(&:to_sym)
|
|
42
|
+
errors.concat(entry_errors(entry, label:, seen:))
|
|
43
|
+
seen << entry[:id]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
errors
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def host_entries
|
|
50
|
+
Array(CommandTower.config.messaging.sounds)
|
|
51
|
+
end
|
|
52
|
+
private_class_method :host_entries
|
|
53
|
+
|
|
54
|
+
def entry_errors(entry, label:, seen:)
|
|
55
|
+
id = entry[:id]
|
|
56
|
+
errors = []
|
|
57
|
+
|
|
58
|
+
if !id.is_a?(String) || !id.match?(ID_FORMAT)
|
|
59
|
+
errors << "#{label} id must match #{ID_FORMAT.inspect}"
|
|
60
|
+
elsif seen.include?(id)
|
|
61
|
+
reason = id == SYSTEM_DEFAULT[:id] ? "reserved for System Default" : "duplicated"
|
|
62
|
+
errors << "#{label} id #{id.inspect} is #{reason}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
errors << "#{label} label must be present" unless present_string?(entry[:label])
|
|
66
|
+
errors.concat(asset_errors(entry[:asset], label:))
|
|
67
|
+
errors
|
|
68
|
+
end
|
|
69
|
+
private_class_method :entry_errors
|
|
70
|
+
|
|
71
|
+
def asset_errors(asset, label:)
|
|
72
|
+
return ["#{label} asset must be present"] unless present_string?(asset)
|
|
73
|
+
return [] if ASSET_EXTENSIONS.include?(File.extname(asset).downcase)
|
|
74
|
+
|
|
75
|
+
["#{label} asset must end in #{ASSET_EXTENSIONS.join(', ')}"]
|
|
76
|
+
end
|
|
77
|
+
private_class_method :asset_errors
|
|
78
|
+
|
|
79
|
+
def present_string?(value)
|
|
80
|
+
value.is_a?(String) && !value.strip.empty?
|
|
81
|
+
end
|
|
82
|
+
private_class_method :present_string?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -31,13 +31,19 @@ module CommandTower
|
|
|
31
31
|
return
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
deep_link = CommandTower.config.messaging.expo.resolve_self_test_deep_link
|
|
35
|
+
if deep_link.nil?
|
|
36
|
+
context.fail!(application_error: CommandTower::Errors::Account::PushTestDestinationError.new)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
produce_result = CommandTower::Services::Messaging::Communications::Produce.call(
|
|
35
41
|
user:,
|
|
36
42
|
notification_type_key: NOTIFICATION_TYPE_KEY,
|
|
37
43
|
host_event_identity: "push_delivery_test/#{user.id}/#{SecureRandom.uuid}",
|
|
38
44
|
title: TITLE,
|
|
39
45
|
body: BODY,
|
|
40
|
-
metadata: {},
|
|
46
|
+
metadata: { "deep_link" => deep_link },
|
|
41
47
|
platform_enabled_channels: CommandTower::Services::Messaging::Preferences::PlatformEnabledChannels.call,
|
|
42
48
|
)
|
|
43
49
|
unless produce_result.success?
|
|
@@ -25,6 +25,15 @@ module CommandTower
|
|
|
25
25
|
return
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
if missing_required_deep_link?
|
|
29
|
+
context.fail!(
|
|
30
|
+
application_error: CommandTower::Errors::ValidationError.new(
|
|
31
|
+
details: { messaging: "deep_link is required" },
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
28
37
|
accept_result = CommandTower::Messaging.accept(
|
|
29
38
|
recipient_id: recipient_result.data[:recipient_id],
|
|
30
39
|
notification_type_key:,
|
|
@@ -65,6 +74,24 @@ module CommandTower
|
|
|
65
74
|
rescue CommandTower::Messaging::Accept::Error
|
|
66
75
|
context.fail!(application_error: CommandTower::Errors::InternalError.new)
|
|
67
76
|
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def missing_required_deep_link?
|
|
81
|
+
return false unless CommandTower::Messaging::NotificationTypes.registered?(notification_type_key)
|
|
82
|
+
|
|
83
|
+
declaration = CommandTower::Messaging::NotificationTypes.lookup(notification_type_key)
|
|
84
|
+
return false unless declaration.deep_link_required
|
|
85
|
+
|
|
86
|
+
!metadata_deep_link_present?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def metadata_deep_link_present?
|
|
90
|
+
return false unless metadata.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
value = metadata["deep_link"] || metadata[:deep_link]
|
|
93
|
+
value.is_a?(String) && !value.strip.empty?
|
|
94
|
+
end
|
|
68
95
|
end
|
|
69
96
|
end
|
|
70
97
|
end
|
|
@@ -116,7 +116,11 @@ module CommandTower
|
|
|
116
116
|
validate :user, is_a: User, required: true
|
|
117
117
|
|
|
118
118
|
def call
|
|
119
|
-
with_recipient
|
|
119
|
+
with_recipient do |recipient_id|
|
|
120
|
+
result = CommandTower::Messaging::Inbox.unread_count(recipient_id:)
|
|
121
|
+
context.count = result.count
|
|
122
|
+
context.badge_count = result.badge_count
|
|
123
|
+
end
|
|
120
124
|
end
|
|
121
125
|
end
|
|
122
126
|
|
|
@@ -26,6 +26,7 @@ module CommandTower
|
|
|
26
26
|
CommandTower::Errors::Account::PushoverVerificationFailedError,
|
|
27
27
|
CommandTower::Errors::Account::PushEndpointNotFoundError,
|
|
28
28
|
CommandTower::Errors::Account::PushTestNoEndpointError,
|
|
29
|
+
CommandTower::Errors::Account::PushTestDestinationError,
|
|
29
30
|
CommandTower::Errors::ValidationError
|
|
30
31
|
:unprocessable_entity
|
|
31
32
|
when CommandTower::Errors::Account::PhoneVerificationThrottledError,
|
|
@@ -154,7 +154,7 @@ module CommandTower
|
|
|
154
154
|
|
|
155
155
|
def load_delivery(channel_delivery_id)
|
|
156
156
|
CommandTower::Messaging::ChannelDelivery.includes(
|
|
157
|
-
communication: %i[user destination_plan],
|
|
157
|
+
communication: %i[user destination_plan inbox_item],
|
|
158
158
|
).find_by(id: channel_delivery_id)
|
|
159
159
|
end
|
|
160
160
|
|
|
@@ -172,6 +172,7 @@ module CommandTower
|
|
|
172
172
|
communication:,
|
|
173
173
|
channel_key: delivery.channel_key,
|
|
174
174
|
recipient_address:,
|
|
175
|
+
channel_delivery: delivery,
|
|
175
176
|
)
|
|
176
177
|
|
|
177
178
|
CommandTower::Messaging::Execution::OperationLogger.render_succeeded(
|
|
@@ -204,18 +205,21 @@ module CommandTower
|
|
|
204
205
|
CommandTower::Messaging::Execution::OperationLogger.adapter_accepted(
|
|
205
206
|
channel_delivery: delivery,
|
|
206
207
|
delivery_attempt: attempt,
|
|
208
|
+
provider_metadata: result.provider_metadata,
|
|
207
209
|
)
|
|
208
210
|
elsif result.retryable_failure?
|
|
209
211
|
CommandTower::Messaging::Execution::OperationLogger.adapter_retryable(
|
|
210
212
|
channel_delivery: delivery,
|
|
211
213
|
delivery_attempt: attempt,
|
|
212
214
|
error_code: result.error_code,
|
|
215
|
+
provider_metadata: result.provider_metadata,
|
|
213
216
|
)
|
|
214
217
|
else
|
|
215
218
|
CommandTower::Messaging::Execution::OperationLogger.adapter_terminal(
|
|
216
219
|
channel_delivery: delivery,
|
|
217
220
|
delivery_attempt: attempt,
|
|
218
221
|
error_code: result.error_code,
|
|
222
|
+
provider_metadata: result.provider_metadata,
|
|
219
223
|
)
|
|
220
224
|
end
|
|
221
225
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Upgrade: CommandTower 0.19.0
|
|
2
|
+
|
|
3
|
+
**From:** `0.18.0`
|
|
4
|
+
**To:** `0.19.0`
|
|
5
|
+
|
|
6
|
+
Minor release: how a push presents itself, and Expo ticket and receipt facts. Notification types can declare badge eligibility, urgency, sound, and a required deep link. Preferences can store urgency and sound. Expo send records provider metadata, and the existing recovery job collects receipts afterward. Two new migrations.
|
|
7
|
+
|
|
8
|
+
## Host-visible changes
|
|
9
|
+
|
|
10
|
+
| Change | Host impact |
|
|
11
|
+
|--------|-------------|
|
|
12
|
+
| Notification type declaration | Optional `badge_eligible` (default false), `urgency`, `sound`, and `deep_link_required` (default false). Omitted urgency and sound stay fixed at `normal` and `default`. |
|
|
13
|
+
| `config.messaging.sounds` | Host entries `{ id:, label:, asset: }` appended after System Default (`id: "default"`, no asset). Asset names end in `.wav`, `.aiff`, or `.caf`. Invalid catalogs fail registry seal. |
|
|
14
|
+
| `config.messaging.expo.home_deep_link` | String or zero-argument Proc. Required when a push-capable type is registered and the Expo adapter is not `disabled`. Used when a push has no metadata `deep_link` and no live inbox item. |
|
|
15
|
+
| `config.messaging.expo.self_test_deep_link` | String or zero-argument Proc. Resolved when `POST /me/push/test` runs. Blank fails the test before Produce. |
|
|
16
|
+
| `config.messaging.expo.interruption_levels` | Subset of `normal`, `time_sensitive`, `critical`. Default is `["normal"]`. A declared urgency above this set degrades `critical` → `time_sensitive` → `normal`. |
|
|
17
|
+
| Produce `metadata.deep_link` | Required when the type sets `deep_link_required` and the value is missing. Push-capable types that are not inbox-backed should set that flag. |
|
|
18
|
+
| Preferences | `GET` catalog entries include `urgency` and `sound`. `PATCH` accepts `preferences.urgency` and `preferences.sound` alongside inbox and channels. |
|
|
19
|
+
| Inbox unread | `Messaging::Inbox.unread_count` returns `count` and `badge_count`. `badge_count` is unread items whose type is badge-eligible. Expo `badge` is sent only for those types, including `0`. |
|
|
20
|
+
| Push thread | A communication with `conversation_key` sends that key as the push thread id. |
|
|
21
|
+
| Delivery attempts | Nullable `provider_metadata` JSON stores Expo ticket and receipt facts. `finished_at` is indexed for receipt collection. |
|
|
22
|
+
| Recovery | After the existing redrive, receipt collection runs. A collection error is logged and does not fail the redrive. |
|
|
23
|
+
|
|
24
|
+
## Host actions
|
|
25
|
+
|
|
26
|
+
1. Bump gem to `0.19.0` (or `>= 0.19.0`).
|
|
27
|
+
2. Install and migrate:
|
|
28
|
+
- `bundle exec rails command_tower:install:migrations`
|
|
29
|
+
- `bundle exec rails db:migrate`
|
|
30
|
+
- New files: `20261012000001_add_provider_metadata_to_messaging_delivery_attempts`, `20261012000002_add_finished_at_index_to_messaging_delivery_attempts`.
|
|
31
|
+
3. If Expo is not `disabled` and any type allows push, set `config.messaging.expo.home_deep_link`. Registry seal raises without it.
|
|
32
|
+
4. Set `self_test_deep_link` before members use push self-test.
|
|
33
|
+
5. Add `time_sensitive` to `interruption_levels` only after the shipped iOS binary has the Time Sensitive capability. Leave `critical` off unless Apple granted Critical Alerts.
|
|
34
|
+
6. Existing declarations that omit the new fields keep today's behavior: no badge, fixed normal urgency, System Default sound, deep link not required.
|
|
35
|
+
|
|
36
|
+
## Not in this release
|
|
37
|
+
|
|
38
|
+
- No host notification types. Hosts still register their own keys.
|
|
39
|
+
- No bundled custom sound files. The catalog stores the asset name. The app binary supplies the file.
|
|
40
|
+
- No backfill of `provider_metadata`. Existing attempts stay null.
|
data/docs/upgrades/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Host-facing upgrade / change summaries for CommandTower releases.
|
|
|
4
4
|
|
|
5
5
|
| Version | Summary |
|
|
6
6
|
|---------|---------|
|
|
7
|
+
| [0.19.0](0.19.0.md) | Push presentation: badge, urgency, sound, deep links; Expo ticket and receipt metadata; preference urgency and sound |
|
|
7
8
|
| [0.18.0](0.18.0.md) | Inbox conversations: optional Produce identity, grouped list entries, `GET /me/inbox/conversation` window of at most 5, accept-time presentation snapshot |
|
|
8
9
|
| [0.17.0](0.17.0.md) | Client-version-compatibility gating (`observe`/`enforce`); Rich Messaging Inbox `content` documents; host-overridable rendering templates; shared email theming |
|
|
9
10
|
| [0.16.0](0.16.0.md) | Me experience-states (`GET`/`POST complete`); `host_key`; `user_experience_states` migration; `me_experience_states` RBAC |
|
|
@@ -46,6 +46,16 @@ module CommandTower
|
|
|
46
46
|
default: ->(_selection) { [] },
|
|
47
47
|
default_shown: "->(_selection) { [] }"
|
|
48
48
|
|
|
49
|
+
add_composer :sounds,
|
|
50
|
+
desc: "Host-registered push sounds appended to the engine's System Default " \
|
|
51
|
+
"(id \"default\", no bundled asset). Array of { id:, label:, asset: } hashes; " \
|
|
52
|
+
"`asset` is the native file reference. Ids are lowercase snake_case and unique. " \
|
|
53
|
+
"Physical asset availability is verified by the iOS app, not here. " \
|
|
54
|
+
"Validated at notification-type registry seal.",
|
|
55
|
+
allowed: Array,
|
|
56
|
+
default: [],
|
|
57
|
+
default_shown: "[]"
|
|
58
|
+
|
|
49
59
|
add_composer :sms,
|
|
50
60
|
desc: "Messaging notification SMS delivery configuration (not Identity OTP)",
|
|
51
61
|
allowed: Sms,
|
|
@@ -9,6 +9,9 @@ module CommandTower
|
|
|
9
9
|
include ClassComposer::Generator
|
|
10
10
|
|
|
11
11
|
ADAPTERS = %w[disabled fake log http].freeze
|
|
12
|
+
# Mirrors Messaging::NotificationTypes::URGENCY_LEVELS. Kept literal because this file
|
|
13
|
+
# loads before the app's autoloaded constants; a spec keeps the two in step.
|
|
14
|
+
INTERRUPTION_LEVELS = %w[normal time_sensitive critical].freeze
|
|
12
15
|
|
|
13
16
|
add_composer :adapter,
|
|
14
17
|
desc: "Expo Push delivery: disabled | fake | log | http. Default disabled (fail closed).",
|
|
@@ -45,6 +48,64 @@ module CommandTower
|
|
|
45
48
|
"(default 3600 = one hour).",
|
|
46
49
|
allowed: Integer,
|
|
47
50
|
default: 3600
|
|
51
|
+
|
|
52
|
+
add_composer :home_deep_link,
|
|
53
|
+
desc: "Host Home deep link used when a push has no metadata deep_link and no live inbox item. " \
|
|
54
|
+
"String or zero-argument Proc. Seal checks presence and does not call a Proc. " \
|
|
55
|
+
"Required when a push-capable type is registered and the Expo adapter is not disabled.",
|
|
56
|
+
allowed: [String, Proc, NilClass],
|
|
57
|
+
default: nil
|
|
58
|
+
|
|
59
|
+
add_composer :self_test_deep_link,
|
|
60
|
+
desc: "Deep link for the account push self-test. String or zero-argument Proc. " \
|
|
61
|
+
"Resolved only when SendSelfTest runs. Blank fails the test before Produce.",
|
|
62
|
+
allowed: [String, Proc, NilClass],
|
|
63
|
+
default: nil
|
|
64
|
+
|
|
65
|
+
add_composer :interruption_levels,
|
|
66
|
+
desc: "Urgency levels this host's shipped iOS binary can honor, as Expo `interruptionLevel`. " \
|
|
67
|
+
"Subset of normal, time_sensitive, critical. `normal` is always supported. A declared " \
|
|
68
|
+
"urgency above this set degrades critical -> time_sensitive -> normal. Add `time_sensitive` " \
|
|
69
|
+
"only after the binary carries the Time Sensitive capability, and `critical` only with " \
|
|
70
|
+
"Apple's Critical Alerts entitlement. Default normal only.",
|
|
71
|
+
allowed: Array,
|
|
72
|
+
default: ["normal"],
|
|
73
|
+
default_shown: '["normal"]',
|
|
74
|
+
validator: ->(val) { val.all? { |level| INTERRUPTION_LEVELS.include?(level) } },
|
|
75
|
+
invalid_message: ->(val) { "Provided #{val.inspect}. Allowed levels: #{INTERRUPTION_LEVELS.join(', ')}" }
|
|
76
|
+
|
|
77
|
+
def home_deep_link_configured?
|
|
78
|
+
link_configured?(home_deep_link)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def resolve_home_deep_link
|
|
82
|
+
resolve_link(home_deep_link)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self_test_deep_link_configured?
|
|
86
|
+
link_configured?(self_test_deep_link)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolve_self_test_deep_link
|
|
90
|
+
resolve_link(self_test_deep_link)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def link_configured?(value)
|
|
96
|
+
return true if value.respond_to?(:call)
|
|
97
|
+
return false unless value.is_a?(String)
|
|
98
|
+
|
|
99
|
+
!value.strip.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def resolve_link(value)
|
|
103
|
+
resolved = value.respond_to?(:call) ? value.call : value
|
|
104
|
+
return nil unless resolved.is_a?(String)
|
|
105
|
+
|
|
106
|
+
stripped = resolved.strip
|
|
107
|
+
stripped.empty? ? nil : stripped
|
|
108
|
+
end
|
|
48
109
|
end
|
|
49
110
|
end
|
|
50
111
|
end
|
|
@@ -18,6 +18,8 @@ module CommandTower
|
|
|
18
18
|
20260906180000_create_user_experience_states.rb
|
|
19
19
|
20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb
|
|
20
20
|
20261010000002_add_conversation_identity_to_messaging_communications.rb
|
|
21
|
+
20261012000001_add_provider_metadata_to_messaging_delivery_attempts.rb
|
|
22
|
+
20261012000002_add_finished_at_index_to_messaging_delivery_attempts.rb
|
|
21
23
|
].freeze
|
|
22
24
|
|
|
23
25
|
module_function
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: command_tower
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- matt-taylor
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: phonelib
|
|
@@ -275,6 +275,7 @@ files:
|
|
|
275
275
|
- app/errors/command_tower/errors/account/phone_verification_throttled_error.rb
|
|
276
276
|
- app/errors/command_tower/errors/account/push_capability_unavailable_error.rb
|
|
277
277
|
- app/errors/command_tower/errors/account/push_endpoint_not_found_error.rb
|
|
278
|
+
- app/errors/command_tower/errors/account/push_test_destination_error.rb
|
|
278
279
|
- app/errors/command_tower/errors/account/push_test_no_endpoint_error.rb
|
|
279
280
|
- app/errors/command_tower/errors/account/push_test_rate_limit_error.rb
|
|
280
281
|
- app/errors/command_tower/errors/account/pushover_already_configured_error.rb
|
|
@@ -503,7 +504,9 @@ files:
|
|
|
503
504
|
- app/services/command_tower/messaging/execution/adapters/email/configuration.rb
|
|
504
505
|
- app/services/command_tower/messaging/execution/adapters/expo/adapter.rb
|
|
505
506
|
- app/services/command_tower/messaging/execution/adapters/expo/configuration.rb
|
|
507
|
+
- app/services/command_tower/messaging/execution/adapters/expo/endpoint_ticket_history.rb
|
|
506
508
|
- app/services/command_tower/messaging/execution/adapters/expo/http_client.rb
|
|
509
|
+
- app/services/command_tower/messaging/execution/adapters/expo/interruption_level.rb
|
|
507
510
|
- app/services/command_tower/messaging/execution/adapters/fake_adapter.rb
|
|
508
511
|
- app/services/command_tower/messaging/execution/adapters/pushover/adapter.rb
|
|
509
512
|
- app/services/command_tower/messaging/execution/adapters/pushover/configuration.rb
|
|
@@ -514,6 +517,7 @@ files:
|
|
|
514
517
|
- app/services/command_tower/messaging/execution/adapters/unconfigured_adapter.rb
|
|
515
518
|
- app/services/command_tower/messaging/execution/attempt_ledger.rb
|
|
516
519
|
- app/services/command_tower/messaging/execution/claim.rb
|
|
520
|
+
- app/services/command_tower/messaging/execution/expo_receipt_collection.rb
|
|
517
521
|
- app/services/command_tower/messaging/execution/invalid_adapter_contract_error.rb
|
|
518
522
|
- app/services/command_tower/messaging/execution/operation_logger.rb
|
|
519
523
|
- app/services/command_tower/messaging/execution/readiness_terminal_mapping.rb
|
|
@@ -549,6 +553,7 @@ files:
|
|
|
549
553
|
- app/services/command_tower/messaging/notification_types/error.rb
|
|
550
554
|
- app/services/command_tower/messaging/notification_types/invalid_declaration_error.rb
|
|
551
555
|
- app/services/command_tower/messaging/notification_types/not_found_error.rb
|
|
556
|
+
- app/services/command_tower/messaging/notification_types/permitted_choice.rb
|
|
552
557
|
- app/services/command_tower/messaging/notification_types/registry.rb
|
|
553
558
|
- app/services/command_tower/messaging/notification_types/sealed_registry_error.rb
|
|
554
559
|
- app/services/command_tower/messaging/planner.rb
|
|
@@ -567,6 +572,7 @@ files:
|
|
|
567
572
|
- app/services/command_tower/messaging/preferences/catalog_category_result.rb
|
|
568
573
|
- app/services/command_tower/messaging/preferences/catalog_notification_result.rb
|
|
569
574
|
- app/services/command_tower/messaging/preferences/catalog_result.rb
|
|
575
|
+
- app/services/command_tower/messaging/preferences/effective_choice.rb
|
|
570
576
|
- app/services/command_tower/messaging/preferences/error.rb
|
|
571
577
|
- app/services/command_tower/messaging/preferences/evaluation_result.rb
|
|
572
578
|
- app/services/command_tower/messaging/preferences/evaluator.rb
|
|
@@ -601,12 +607,14 @@ files:
|
|
|
601
607
|
- app/services/command_tower/messaging/rendering/inbox_document_renderer.rb
|
|
602
608
|
- app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb
|
|
603
609
|
- app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb
|
|
610
|
+
- app/services/command_tower/messaging/rendering/push_presentation.rb
|
|
604
611
|
- app/services/command_tower/messaging/rendering/render_error.rb
|
|
605
612
|
- app/services/command_tower/messaging/rendering/rendered_payload.rb
|
|
606
613
|
- app/services/command_tower/messaging/rendering/rendered_push_payload.rb
|
|
607
614
|
- app/services/command_tower/messaging/rendering/rendered_pushover_payload.rb
|
|
608
615
|
- app/services/command_tower/messaging/rendering/rendered_sms_payload.rb
|
|
609
616
|
- app/services/command_tower/messaging/rendering/template_resolver.rb
|
|
617
|
+
- app/services/command_tower/messaging/sound_catalog.rb
|
|
610
618
|
- app/services/command_tower/secrets.rb
|
|
611
619
|
- app/services/command_tower/secrets/cleanse.rb
|
|
612
620
|
- app/services/command_tower/secrets/generate.rb
|
|
@@ -800,6 +808,8 @@ files:
|
|
|
800
808
|
- db/migrate/20260906180000_create_user_experience_states.rb
|
|
801
809
|
- db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb
|
|
802
810
|
- db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb
|
|
811
|
+
- db/migrate/20261012000001_add_provider_metadata_to_messaging_delivery_attempts.rb
|
|
812
|
+
- db/migrate/20261012000002_add_finished_at_index_to_messaging_delivery_attempts.rb
|
|
803
813
|
- docs/admin_workspace.md
|
|
804
814
|
- docs/api_reference.md
|
|
805
815
|
- docs/architecture.md
|
|
@@ -849,6 +859,7 @@ files:
|
|
|
849
859
|
- docs/upgrades/0.16.0.md
|
|
850
860
|
- docs/upgrades/0.17.0.md
|
|
851
861
|
- docs/upgrades/0.18.0.md
|
|
862
|
+
- docs/upgrades/0.19.0.md
|
|
852
863
|
- docs/upgrades/README.md
|
|
853
864
|
- lib/command_tower.rb
|
|
854
865
|
- lib/command_tower/admin_scope.rb
|