sincerely 1.0.0 → 1.1.1

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/app/assets/config/sincerely.js +2 -0
  4. data/app/assets/images/sincerely/arrow_left.svg +1 -0
  5. data/app/assets/images/sincerely/check.svg +3 -0
  6. data/app/assets/images/sincerely/chevron_left.svg +1 -0
  7. data/app/assets/images/sincerely/clock.svg +1 -0
  8. data/app/assets/images/sincerely/code.svg +1 -0
  9. data/app/assets/images/sincerely/eye.svg +1 -0
  10. data/app/assets/images/sincerely/logout.svg +1 -0
  11. data/app/assets/images/sincerely/menu.svg +1 -0
  12. data/app/assets/images/sincerely/moon.svg +1 -0
  13. data/app/assets/images/sincerely/pencil.svg +4 -0
  14. data/app/assets/images/sincerely/plus.svg +3 -0
  15. data/app/assets/images/sincerely/progress_spinner.svg +3 -0
  16. data/app/assets/images/sincerely/spinner.svg +3 -0
  17. data/app/assets/images/sincerely/sun.svg +1 -0
  18. data/app/assets/images/sincerely/warning_circle.svg +4 -0
  19. data/app/assets/javascripts/sincerely/charts.js +115 -0
  20. data/app/assets/javascripts/sincerely/clickable_rows.js +29 -0
  21. data/app/assets/javascripts/sincerely/layout.js +104 -0
  22. data/app/assets/javascripts/sincerely/send_form.js +243 -0
  23. data/app/assets/javascripts/sincerely/template_form.js +44 -0
  24. data/app/assets/javascripts/sincerely/template_preview.js +12 -0
  25. data/app/assets/javascripts/sincerely/theme_init.js +9 -0
  26. data/app/assets/stylesheets/sincerely/charts.css +250 -0
  27. data/app/assets/stylesheets/sincerely/components.css +632 -0
  28. data/app/assets/stylesheets/sincerely/layout.css +233 -0
  29. data/app/assets/stylesheets/sincerely/navigation.css +198 -0
  30. data/app/assets/stylesheets/sincerely/send_form.css +287 -0
  31. data/app/assets/stylesheets/sincerely/template_form.css +74 -0
  32. data/app/assets/stylesheets/sincerely/template_preview.css +75 -0
  33. data/app/assets/stylesheets/sincerely/variables.css +88 -0
  34. data/app/controllers/sincerely/application_controller.rb +3 -72
  35. data/app/controllers/sincerely/concerns/authorization.rb +21 -0
  36. data/app/controllers/sincerely/concerns/delivery_metrics.rb +51 -0
  37. data/app/controllers/sincerely/concerns/event_filtering.rb +58 -0
  38. data/app/controllers/sincerely/concerns/event_timeline.rb +41 -0
  39. data/app/controllers/sincerely/concerns/notification_timeline.rb +73 -0
  40. data/app/controllers/sincerely/concerns/notifications.rb +47 -0
  41. data/app/controllers/sincerely/concerns/pagination.rb +39 -0
  42. data/app/controllers/sincerely/concerns/periods.rb +40 -0
  43. data/app/controllers/sincerely/dashboard_controller.rb +30 -88
  44. data/app/controllers/sincerely/delivery_events_controller.rb +13 -21
  45. data/app/controllers/sincerely/engagement_events_controller.rb +13 -22
  46. data/app/controllers/sincerely/notifications_controller.rb +68 -49
  47. data/app/controllers/sincerely/send_controller.rb +82 -67
  48. data/app/controllers/sincerely/templates_controller.rb +37 -8
  49. data/app/helpers/sincerely/application_helper.rb +22 -0
  50. data/app/helpers/sincerely/chart_helper.rb +79 -0
  51. data/app/views/layouts/sincerely/application.html.erb +24 -528
  52. data/app/views/sincerely/dashboard/index.html.erb +25 -256
  53. data/app/views/sincerely/delivery_events/index.html.erb +6 -38
  54. data/app/views/sincerely/engagement_events/index.html.erb +6 -38
  55. data/app/views/sincerely/notifications/index.html.erb +7 -23
  56. data/app/views/sincerely/notifications/show.html.erb +4 -4
  57. data/app/views/sincerely/send/new.html.erb +8 -522
  58. data/app/views/sincerely/shared/_event.html.erb +21 -0
  59. data/app/views/sincerely/shared/_line_chart.html.erb +38 -0
  60. data/app/views/sincerely/shared/_notification_type_filter.html.erb +9 -0
  61. data/app/views/sincerely/shared/_status_option.html.erb +1 -0
  62. data/app/views/sincerely/shared/_template_option.html.erb +3 -0
  63. data/app/views/sincerely/templates/_form.html.erb +8 -107
  64. data/app/views/sincerely/templates/edit.html.erb +2 -2
  65. data/app/views/sincerely/templates/index.html.erb +7 -15
  66. data/app/views/sincerely/templates/new.html.erb +2 -2
  67. data/app/views/sincerely/templates/preview.html.erb +7 -26
  68. data/app/views/sincerely/templates/show.html.erb +3 -3
  69. data/lib/config/sincerely_config.rb +2 -2
  70. data/lib/sincerely/delivery_systems/email_aws_ses.rb +53 -19
  71. data/lib/sincerely/engine.rb +4 -0
  72. data/lib/sincerely/mixins/notification_model.rb +8 -2
  73. data/lib/sincerely/services/process_delivery_event.rb +9 -7
  74. data/lib/sincerely/templates/notification_template.rb +1 -1
  75. data/lib/sincerely/version.rb +1 -1
  76. data/lib/sincerely.rb +1 -1
  77. metadata +66 -4
  78. data/sincerely.gemspec +0 -37
@@ -1,59 +1,5 @@
1
- <style>
2
- .preview-tabs {
3
- display: flex;
4
- gap: 0;
5
- margin-bottom: -1px;
6
- position: relative;
7
- z-index: 1;
8
- }
9
- .preview-tab {
10
- padding: 0.5rem 1rem;
11
- background: var(--bg-secondary);
12
- border: 1px solid var(--border-color);
13
- border-bottom: none;
14
- border-radius: 0.375rem 0.375rem 0 0;
15
- cursor: pointer;
16
- font-size: 0.8125rem;
17
- font-weight: 500;
18
- color: var(--text-secondary);
19
- transition: all 0.15s;
20
- display: flex;
21
- align-items: center;
22
- gap: 0.375rem;
23
- }
24
- .preview-tab:hover { color: var(--text-primary); }
25
- .preview-tab.active {
26
- background: var(--bg-primary);
27
- color: var(--primary);
28
- border-bottom: 1px solid var(--bg-primary);
29
- }
30
- .preview-tab svg { width: 0.875rem; height: 0.875rem; }
31
- .preview-container { position: relative; }
32
- .preview-panel { display: none; }
33
- .preview-panel.active { display: block; }
34
- .preview-frame {
35
- width: 100%;
36
- min-height: 300px;
37
- border: 1px solid var(--border-color);
38
- border-radius: 0 0.375rem 0.375rem 0.375rem;
39
- background: #fff;
40
- }
41
- .preview-text {
42
- width: 100%;
43
- min-height: 200px;
44
- padding: 1rem;
45
- border: 1px solid var(--border-color);
46
- border-radius: 0 0.375rem 0.375rem 0.375rem;
47
- background: var(--bg-primary);
48
- font-family: 'SF Mono', Consolas, monospace;
49
- font-size: 0.8125rem;
50
- white-space: pre-wrap;
51
- word-break: break-word;
52
- color: var(--text-primary);
53
- margin: 0;
54
- overflow: auto;
55
- }
56
- </style>
1
+ <%= stylesheet_link_tag 'sincerely/template_form' %>
2
+
57
3
 
58
4
  <%= form_with model: template, url: form_url, local: true, class: 'template-form' do |f| %>
59
5
  <% if template.errors.any? %>
@@ -121,11 +67,11 @@
121
67
  <div class="preview-container" data-preview="html">
122
68
  <div class="preview-tabs">
123
69
  <button type="button" class="preview-tab active" data-target="code">
124
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
70
+ <%= inline_svg 'sincerely/code' %>
125
71
  Code
126
72
  </button>
127
73
  <button type="button" class="preview-tab" data-target="preview">
128
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
74
+ <%= inline_svg 'sincerely/eye' %>
129
75
  Preview
130
76
  </button>
131
77
  </div>
@@ -152,11 +98,11 @@
152
98
  <div class="preview-container" data-preview="text">
153
99
  <div class="preview-tabs">
154
100
  <button type="button" class="preview-tab active" data-target="code">
155
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
101
+ <%= inline_svg 'sincerely/code' %>
156
102
  Code
157
103
  </button>
158
104
  <button type="button" class="preview-tab" data-target="preview">
159
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
105
+ <%= inline_svg 'sincerely/eye' %>
160
106
  Preview
161
107
  </button>
162
108
  </div>
@@ -174,53 +120,8 @@ Welcome to our platform.' %>
174
120
 
175
121
  <div class="form-actions">
176
122
  <%= f.submit template.persisted? ? 'Update Template' : 'Create Template', class: 'btn btn-primary' %>
177
- <a href="<%= template.persisted? ? sincerely.template_path(template) : sincerely.templates_path %>" class="btn btn-secondary">Cancel</a>
123
+ <a href="<%= template.persisted? ? template_path(template) : templates_path %>" class="btn btn-secondary">Cancel</a>
178
124
  </div>
179
125
  <% end %>
180
126
 
181
- <script>
182
- (function() {
183
- function updateHtmlPreview() {
184
- const textarea = document.getElementById('html_content');
185
- const iframe = document.getElementById('html_preview');
186
- if (!textarea || !iframe) return;
187
-
188
- iframe.srcdoc = textarea.value || '';
189
- }
190
-
191
- function updateTextPreview() {
192
- const textarea = document.getElementById('text_content');
193
- const preview = document.getElementById('text_preview');
194
- if (!textarea || !preview) return;
195
-
196
- preview.textContent = textarea.value || '';
197
- }
198
-
199
- document.querySelectorAll('.preview-container').forEach(container => {
200
- const tabs = container.querySelectorAll('.preview-tab');
201
- const panels = container.querySelectorAll('.preview-panel');
202
- const previewType = container.dataset.preview;
203
-
204
- tabs.forEach(tab => {
205
- tab.addEventListener('click', () => {
206
- const target = tab.dataset.target;
207
-
208
- tabs.forEach(t => t.classList.remove('active'));
209
- tab.classList.add('active');
210
-
211
- panels.forEach(p => {
212
- p.classList.toggle('active', p.dataset.panel === target);
213
- });
214
-
215
- if (target === 'preview') {
216
- if (previewType === 'html') {
217
- updateHtmlPreview();
218
- } else if (previewType === 'text') {
219
- updateTextPreview();
220
- }
221
- }
222
- });
223
- });
224
- });
225
- })();
226
- </script>
127
+ <%= javascript_include_tag 'sincerely/template_form' %>
@@ -1,11 +1,11 @@
1
1
  <div class="page-header page-header--visible">
2
2
  <div class="page-header-top">
3
3
  <button onclick="history.back()" class="btn-back" title="Back">
4
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
4
+ <%= inline_svg 'sincerely/chevron_left' %>
5
5
  </button>
6
6
  <h1 class="page-title">Edit Template</h1>
7
7
  </div>
8
8
  <p class="page-subtitle">Update <%= @template.name %></p>
9
9
  </div>
10
10
 
11
- <%= render 'form', template: @template, form_url: sincerely.template_path(@template) %>
11
+ <%= render 'form', template: @template, form_url: template_path(@template) %>
@@ -6,10 +6,8 @@
6
6
  <div class="card">
7
7
  <div class="card-header">
8
8
  <h2 class="card-title">All Templates</h2>
9
- <a href="<%= sincerely.new_template_path %>" class="btn btn-primary">
10
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
11
- <path d="M12 5v14M5 12h14"></path>
12
- </svg>
9
+ <a href="<%= new_template_path %>" class="btn btn-primary">
10
+ <%= inline_svg 'sincerely/plus' %>
13
11
  New Template
14
12
  </a>
15
13
  </div>
@@ -32,24 +30,18 @@
32
30
  <td><%= truncate(template.sender, length: 30) %></td>
33
31
  <td><%= template.created_at.strftime('%b %d, %Y') %></td>
34
32
  <td class="actions-cell">
35
- <a href="<%= sincerely.template_path(template) %>" class="btn-icon" title="View">
36
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
37
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
38
- <circle cx="12" cy="12" r="3"></circle>
39
- </svg>
33
+ <a href="<%= template_path(template) %>" class="btn-icon" title="View">
34
+ <%= inline_svg 'sincerely/eye' %>
40
35
  </a>
41
- <a href="<%= sincerely.edit_template_path(template) %>" class="btn-icon" title="Edit">
42
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
43
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
44
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
45
- </svg>
36
+ <a href="<%= edit_template_path(template) %>" class="btn-icon" title="Edit">
37
+ <%= inline_svg 'sincerely/pencil' %>
46
38
  </a>
47
39
  </td>
48
40
  </tr>
49
41
  <% end %>
50
42
  <% if @templates.empty? %>
51
43
  <tr>
52
- <td colspan="5" class="empty-state">No templates found. <a href="<%= sincerely.new_template_path %>">Create one</a></td>
44
+ <td colspan="5" class="empty-state">No templates found. <a href="<%= new_template_path %>">Create one</a></td>
53
45
  </tr>
54
46
  <% end %>
55
47
  </tbody>
@@ -1,11 +1,11 @@
1
1
  <div class="page-header page-header--visible">
2
2
  <div class="page-header-top">
3
3
  <button onclick="history.back()" class="btn-back" title="Back">
4
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
4
+ <%= inline_svg 'sincerely/chevron_left' %>
5
5
  </button>
6
6
  <h1 class="page-title">New Template</h1>
7
7
  </div>
8
8
  <p class="page-subtitle">Create a new notification template</p>
9
9
  </div>
10
10
 
11
- <%= render 'form', template: @template, form_url: sincerely.templates_path %>
11
+ <%= render 'form', template: @template, form_url: templates_path %>
@@ -2,20 +2,8 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Preview: <%= @template.name %></title>
5
- <style>
6
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background: #f5f5f5; }
7
- .preview-header { background: #1f2937; color: white; padding: 1rem 2rem; }
8
- .preview-header h1 { margin: 0; font-size: 1.25rem; }
9
- .preview-meta { margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.8; }
10
- .preview-tabs { display: flex; gap: 0; background: #374151; }
11
- .preview-tab { padding: 0.75rem 1.5rem; color: #9ca3af; cursor: pointer; border: none; background: none; font-size: 0.875rem; }
12
- .preview-tab.active { color: white; background: #4f46e5; }
13
- .preview-content { padding: 2rem; }
14
- .preview-frame { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
15
- .preview-frame-html { padding: 0; }
16
- .preview-frame-html iframe { width: 100%; height: 600px; border: none; }
17
- .preview-frame-text { padding: 2rem; white-space: pre-wrap; font-family: monospace; background: #fafafa; }
18
- </style>
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'">
6
+ <%= stylesheet_link_tag 'sincerely/template_preview' %>
19
7
  </head>
20
8
  <body>
21
9
  <div class="preview-header">
@@ -25,24 +13,17 @@
25
13
  </div>
26
14
  </div>
27
15
  <div class="preview-tabs">
28
- <button class="preview-tab active" onclick="showTab('html')">HTML</button>
29
- <button class="preview-tab" onclick="showTab('text')">Text</button>
16
+ <button class="preview-tab active" data-tab="html">HTML</button>
17
+ <button class="preview-tab" data-tab="text">Text</button>
30
18
  </div>
31
19
  <div class="preview-content">
32
20
  <div id="html-preview" class="preview-frame preview-frame-html">
33
- <iframe srcdoc="<%= ERB::Util.html_escape(@rendered_html) %>"></iframe>
21
+ <iframe srcdoc="<%= ERB::Util.html_escape(@rendered_html) %>" sandbox="allow-same-origin"></iframe>
34
22
  </div>
35
- <div id="text-preview" class="preview-frame preview-frame-text" style="display: none;">
23
+ <div id="text-preview" class="preview-frame preview-frame-text preview-frame--hidden">
36
24
  <%= @rendered_text %>
37
25
  </div>
38
26
  </div>
39
- <script>
40
- function showTab(tab) {
41
- document.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active'));
42
- document.querySelector('.preview-tab[onclick="showTab(\'' + tab + '\')"]').classList.add('active');
43
- document.getElementById('html-preview').style.display = tab === 'html' ? 'block' : 'none';
44
- document.getElementById('text-preview').style.display = tab === 'text' ? 'block' : 'none';
45
- }
46
- </script>
27
+ <%= javascript_include_tag 'sincerely/template_preview' %>
47
28
  </body>
48
29
  </html>
@@ -1,7 +1,7 @@
1
1
  <div class="page-header page-header--visible">
2
2
  <div class="page-header-top">
3
3
  <button onclick="history.back()" class="btn-back" title="Back">
4
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
4
+ <%= inline_svg 'sincerely/chevron_left' %>
5
5
  </button>
6
6
  <h1 class="page-title"><%= @template.name %></h1>
7
7
  </div>
@@ -13,8 +13,8 @@
13
13
  <div class="card-header">
14
14
  <h2 class="card-title">Details</h2>
15
15
  <div class="header-actions">
16
- <a href="<%= sincerely.preview_template_path(@template) %>" class="btn btn-secondary btn-sm" target="_blank">Preview</a>
17
- <a href="<%= sincerely.edit_template_path(@template) %>" class="btn btn-primary btn-sm">Edit</a>
16
+ <a href="<%= preview_template_path(@template) %>" class="btn btn-secondary btn-sm" target="_blank">Preview</a>
17
+ <a href="<%= edit_template_path(@template) %>" class="btn btn-primary btn-sm">Edit</a>
18
18
  </div>
19
19
  </div>
20
20
  <div class="details-grid">
@@ -11,13 +11,13 @@ module Sincerely
11
11
 
12
12
  # Return navigation (optional)
13
13
  # Set return_url to show a "back to app" link in the footer
14
- attr_accessor :return_url, :return_label
14
+ attr_config :return_url, :return_label
15
15
 
16
16
  # Logout configuration (optional)
17
17
  # Set logout_url to show a logout button in the navigation
18
18
  # The button only appears when authentication is configured
19
19
  # Set logout_method to :delete for Devise (default is :get)
20
- attr_accessor :logout_url, :logout_label, :logout_method
20
+ attr_config :logout_url, :logout_label, :logout_method
21
21
 
22
22
  # Notification filtering (optional)
23
23
  # Lambda that returns a hash for where() clause to filter notifications
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aws-sdk-sesv2'
4
+ require 'mail'
4
5
 
5
6
  module Sincerely
6
7
  module DeliverySystems
@@ -34,31 +35,64 @@ module Sincerely
34
35
  end
35
36
 
36
37
  def email_options
37
- opts = {
38
- from_email_address: template.sender,
39
- destination: { to_addresses: [notification.recipient] },
40
- content: {
41
- simple: {
42
- subject: { data: subject },
43
- body: {
44
- html: {
45
- data: notification.render_content(:html)
46
- },
47
- text: {
48
- data: notification.render_content(:text)
49
- }
50
- }
51
- }
52
- }
53
- }
38
+ opts = { destination: { to_addresses: [notification.recipient] } }
39
+ if attachments.present?
40
+ opts[:content] = { raw: { data: raw_message } }
41
+ else
42
+ opts[:from_email_address] = template.sender
43
+ opts[:content] = { simple: simple_content }
44
+ end
54
45
  config_set = options[:configuration_set_name]
55
46
  opts = opts.merge(configuration_set_name: config_set) if config_set.present?
56
47
  opts
57
48
  end
58
49
 
50
+ def simple_content
51
+ {
52
+ subject: { data: subject },
53
+ body: {
54
+ html: { data: notification.render_content(:html) },
55
+ text: { data: notification.render_content(:text) }
56
+ }
57
+ }
58
+ end
59
+
60
+ def attachments
61
+ notification.delivery_options_hash.fetch('attachments', [])
62
+ end
63
+
64
+ def raw_message
65
+ message = Mail.new
66
+ message.from = template.sender
67
+ message.to = notification.recipient
68
+ message.subject = subject
69
+
70
+ cid_by_variable = attach_inline_images(message)
71
+ text_body = notification.render_content(:text, cid_by_variable)
72
+ html_body = notification.render_content(:html, cid_by_variable)
73
+
74
+ message.text_part = Mail::Part.new { body text_body }
75
+ message.html_part = Mail::Part.new do
76
+ content_type 'text/html; charset=UTF-8'
77
+ body html_body
78
+ end
79
+
80
+ message.to_s
81
+ end
82
+
83
+ def attach_inline_images(message)
84
+ attachments.each_with_object({}) do |attachment, cid_by_variable|
85
+ message.attachments.inline[attachment['filename']] = {
86
+ mime_type: attachment['mime_type'],
87
+ content: attachment['content']
88
+ }
89
+ cid_by_variable[attachment['variable']] = message.attachments[attachment['filename']].url
90
+ end
91
+ end
92
+
59
93
  def subject
60
- raw_subject = notification.delivery_options&.fetch('subject', nil) || template.subject
61
- template.renderer.render(raw_subject, notification.delivery_options&.stringify_keys)
94
+ raw_subject = notification.delivery_options_hash.fetch('subject', nil) || template.subject || ''
95
+ template.renderer.render(raw_subject, notification.delivery_options_hash)
62
96
  end
63
97
 
64
98
  def update_notification(response)
@@ -3,5 +3,9 @@
3
3
  module Sincerely
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace Sincerely
6
+
7
+ initializer 'sincerely.assets.precompile' do |app|
8
+ app.config.assets.precompile += %w[sincerely.js] if app.config.respond_to?(:assets)
9
+ end
6
10
  end
7
11
  end
@@ -67,8 +67,14 @@ module Sincerely
67
67
  end
68
68
  end
69
69
 
70
- def render_content(content_type)
71
- template.render(content_type, delivery_options)
70
+ def render_content(content_type, extra_template_data = {})
71
+ options = delivery_options_hash
72
+ merged_template_data = (options['template_data'] || {}).merge(extra_template_data.stringify_keys)
73
+ template.render(content_type, options.merge('template_data' => merged_template_data))
74
+ end
75
+
76
+ def delivery_options_hash
77
+ delivery_options&.stringify_keys || {}
72
78
  end
73
79
 
74
80
  def deliver
@@ -21,24 +21,26 @@ module Sincerely
21
21
 
22
22
  case event_type.to_sym
23
23
  when :bounce
24
- notification.set_bounced!
24
+ notification.set_bounced! if notification.may_set_bounced?
25
25
  create_event
26
26
  when :complaint
27
- notification.set_complained!
27
+ notification.set_complained! if notification.may_set_complained?
28
28
  create_event
29
29
  when :delivery
30
- notification.set_delivered!
30
+ notification.set_delivered! if notification.may_set_delivered?
31
31
  create_event
32
32
  when :send
33
33
  notification.set_accepted! if notification.may_set_accepted?
34
34
  when :reject
35
- notification.set_rejected!
36
- notification.update(error_message: event.rejection_reason)
35
+ if notification.may_set_rejected?
36
+ notification.set_rejected!
37
+ notification.update(error_message: event.rejection_reason)
38
+ end
37
39
  when :open
38
- notification.set_opened!
40
+ notification.set_opened! if notification.may_set_opened?
39
41
  create_event
40
42
  when :click
41
- notification.set_clicked!
43
+ notification.set_clicked! if notification.may_set_clicked?
42
44
  create_event
43
45
  end
44
46
  end
@@ -10,7 +10,7 @@ module Sincerely
10
10
  validates :name, presence: true
11
11
 
12
12
  def render(content_type, options = {})
13
- content = public_send("#{content_type}_content")
13
+ content = public_send("#{content_type}_content") || ''
14
14
  renderer.render(content, options&.stringify_keys)
15
15
  end
16
16
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sincerely
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.1'
5
5
  end
data/lib/sincerely.rb CHANGED
@@ -17,4 +17,4 @@ require_relative 'sincerely/services/events/aws_ses_complaint_event'
17
17
  require_relative 'sincerely/services/events/aws_ses_open_event'
18
18
  require_relative 'sincerely/templates/email_liquid_template'
19
19
  require_relative 'sincerely/version'
20
- require_relative 'sincerely/engine' if defined?(Rails)
20
+ require_relative 'sincerely/engine' if defined?(Rails::Engine)
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sincerely
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - 100 Starlings
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2026-03-31 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: aasm
@@ -79,6 +80,20 @@ dependencies:
79
80
  - - "~>"
80
81
  - !ruby/object:Gem::Version
81
82
  version: '5.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mail
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.7'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '2.7'
82
97
  - !ruby/object:Gem::Dependency
83
98
  name: railties
84
99
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +108,7 @@ dependencies:
93
108
  - - ">="
94
109
  - !ruby/object:Gem::Version
95
110
  version: '6'
111
+ description:
96
112
  email:
97
113
  - rubyblok@100starlings.com
98
114
  executables: []
@@ -104,7 +120,46 @@ files:
104
120
  - CHANGELOG.md
105
121
  - README.md
106
122
  - Rakefile
123
+ - app/assets/config/sincerely.js
124
+ - app/assets/images/sincerely/arrow_left.svg
125
+ - app/assets/images/sincerely/check.svg
126
+ - app/assets/images/sincerely/chevron_left.svg
127
+ - app/assets/images/sincerely/clock.svg
128
+ - app/assets/images/sincerely/code.svg
129
+ - app/assets/images/sincerely/eye.svg
130
+ - app/assets/images/sincerely/logout.svg
131
+ - app/assets/images/sincerely/menu.svg
132
+ - app/assets/images/sincerely/moon.svg
133
+ - app/assets/images/sincerely/pencil.svg
134
+ - app/assets/images/sincerely/plus.svg
135
+ - app/assets/images/sincerely/progress_spinner.svg
136
+ - app/assets/images/sincerely/spinner.svg
137
+ - app/assets/images/sincerely/sun.svg
138
+ - app/assets/images/sincerely/warning_circle.svg
139
+ - app/assets/javascripts/sincerely/charts.js
140
+ - app/assets/javascripts/sincerely/clickable_rows.js
141
+ - app/assets/javascripts/sincerely/layout.js
142
+ - app/assets/javascripts/sincerely/send_form.js
143
+ - app/assets/javascripts/sincerely/template_form.js
144
+ - app/assets/javascripts/sincerely/template_preview.js
145
+ - app/assets/javascripts/sincerely/theme_init.js
146
+ - app/assets/stylesheets/sincerely/charts.css
147
+ - app/assets/stylesheets/sincerely/components.css
148
+ - app/assets/stylesheets/sincerely/layout.css
149
+ - app/assets/stylesheets/sincerely/navigation.css
150
+ - app/assets/stylesheets/sincerely/send_form.css
151
+ - app/assets/stylesheets/sincerely/template_form.css
152
+ - app/assets/stylesheets/sincerely/template_preview.css
153
+ - app/assets/stylesheets/sincerely/variables.css
107
154
  - app/controllers/sincerely/application_controller.rb
155
+ - app/controllers/sincerely/concerns/authorization.rb
156
+ - app/controllers/sincerely/concerns/delivery_metrics.rb
157
+ - app/controllers/sincerely/concerns/event_filtering.rb
158
+ - app/controllers/sincerely/concerns/event_timeline.rb
159
+ - app/controllers/sincerely/concerns/notification_timeline.rb
160
+ - app/controllers/sincerely/concerns/notifications.rb
161
+ - app/controllers/sincerely/concerns/pagination.rb
162
+ - app/controllers/sincerely/concerns/periods.rb
108
163
  - app/controllers/sincerely/dashboard_controller.rb
109
164
  - app/controllers/sincerely/delivery_events_controller.rb
110
165
  - app/controllers/sincerely/engagement_events_controller.rb
@@ -112,6 +167,7 @@ files:
112
167
  - app/controllers/sincerely/send_controller.rb
113
168
  - app/controllers/sincerely/templates_controller.rb
114
169
  - app/helpers/sincerely/application_helper.rb
170
+ - app/helpers/sincerely/chart_helper.rb
115
171
  - app/views/layouts/sincerely/application.html.erb
116
172
  - app/views/sincerely/dashboard/index.html.erb
117
173
  - app/views/sincerely/delivery_events/index.html.erb
@@ -119,7 +175,12 @@ files:
119
175
  - app/views/sincerely/notifications/index.html.erb
120
176
  - app/views/sincerely/notifications/show.html.erb
121
177
  - app/views/sincerely/send/new.html.erb
178
+ - app/views/sincerely/shared/_event.html.erb
179
+ - app/views/sincerely/shared/_line_chart.html.erb
180
+ - app/views/sincerely/shared/_notification_type_filter.html.erb
122
181
  - app/views/sincerely/shared/_pagination.html.erb
182
+ - app/views/sincerely/shared/_status_option.html.erb
183
+ - app/views/sincerely/shared/_template_option.html.erb
123
184
  - app/views/sincerely/templates/_form.html.erb
124
185
  - app/views/sincerely/templates/edit.html.erb
125
186
  - app/views/sincerely/templates/index.html.erb
@@ -159,7 +220,6 @@ files:
159
220
  - lib/sincerely/templates/email_liquid_template.rb
160
221
  - lib/sincerely/templates/notification_template.rb
161
222
  - lib/sincerely/version.rb
162
- - sincerely.gemspec
163
223
  homepage: http://www.rubyblok.com
164
224
  licenses:
165
225
  - MIT
@@ -167,6 +227,7 @@ metadata:
167
227
  homepage_uri: http://www.rubyblok.com
168
228
  source_code_uri: https://github.com/100Starlings/sincerely
169
229
  rubygems_mfa_required: 'true'
230
+ post_install_message:
170
231
  rdoc_options: []
171
232
  require_paths:
172
233
  - lib
@@ -181,7 +242,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
242
  - !ruby/object:Gem::Version
182
243
  version: '0'
183
244
  requirements: []
184
- rubygems_version: 3.6.2
245
+ rubygems_version: 3.5.22
246
+ signing_key:
185
247
  specification_version: 4
186
248
  summary: Notifications
187
249
  test_files: []