sms_opener 0.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.
@@ -0,0 +1,350 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SMS to <%= CGI.escapeHTML(to_display) %> — SmsOpener</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --card-bg: #1e293b;
11
+ --border: #334155;
12
+ --text: #f8fafc;
13
+ --muted: #94a3b8;
14
+ --accent: #38bdf8;
15
+ --bubble-bg: #2563eb;
16
+ --bubble-text: #ffffff;
17
+ --success: #10b981;
18
+ --warning: #f59e0b;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0;
23
+ padding: 24px 16px;
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
25
+ background-color: var(--bg);
26
+ color: var(--text);
27
+ display: flex;
28
+ justify-content: center;
29
+ min-height: 100vh;
30
+ }
31
+ .container {
32
+ width: 100%;
33
+ max-width: 820px;
34
+ display: grid;
35
+ grid-template-columns: 360px 1fr;
36
+ gap: 24px;
37
+ align-items: start;
38
+ }
39
+ @media (max-width: 760px) {
40
+ .container { grid-template-columns: 1fr; }
41
+ }
42
+ /* Phone device frame */
43
+ .phone-frame {
44
+ background: #090d16;
45
+ border: 4px solid #334155;
46
+ border-radius: 36px;
47
+ padding: 14px;
48
+ box-shadow: 0 20px 45px rgba(0, 0, 0, 0.5);
49
+ }
50
+ .phone-screen {
51
+ background: #111827;
52
+ border-radius: 24px;
53
+ overflow: hidden;
54
+ min-height: 520px;
55
+ display: flex;
56
+ flex-direction: column;
57
+ }
58
+ .phone-statusbar {
59
+ padding: 10px 18px 6px;
60
+ display: flex;
61
+ justify-content: space-between;
62
+ font-size: 12px;
63
+ font-weight: 600;
64
+ color: var(--muted);
65
+ }
66
+ .phone-header {
67
+ padding: 12px 16px;
68
+ background: rgba(30, 41, 59, 0.85);
69
+ border-bottom: 1px solid var(--border);
70
+ text-align: center;
71
+ }
72
+ .avatar-circle {
73
+ width: 40px;
74
+ height: 40px;
75
+ border-radius: 50%;
76
+ background: linear-gradient(135deg, #3b82f6, #6366f1);
77
+ color: #fff;
78
+ display: inline-flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ font-weight: 700;
82
+ font-size: 15px;
83
+ margin-bottom: 4px;
84
+ }
85
+ .sender-label {
86
+ font-size: 13px;
87
+ font-weight: 600;
88
+ color: var(--text);
89
+ word-break: break-all;
90
+ }
91
+ .recipient-sub {
92
+ font-size: 11px;
93
+ color: var(--muted);
94
+ margin-top: 2px;
95
+ }
96
+ .chat-area {
97
+ flex: 1;
98
+ padding: 18px 14px;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 10px;
102
+ }
103
+ .chat-timestamp {
104
+ text-align: center;
105
+ font-size: 11px;
106
+ color: var(--muted);
107
+ margin-bottom: 6px;
108
+ }
109
+ .sms-bubble {
110
+ background: var(--bubble-bg);
111
+ color: var(--bubble-text);
112
+ padding: 12px 15px;
113
+ border-radius: 18px 18px 18px 4px;
114
+ max-width: 90%;
115
+ line-height: 1.45;
116
+ font-size: 14.5px;
117
+ word-wrap: break-word;
118
+ box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25);
119
+ }
120
+ .sms-bubble a.sms-link {
121
+ color: #bae6fd;
122
+ text-decoration: underline;
123
+ word-break: break-all;
124
+ }
125
+ .mms-media {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 8px;
129
+ margin-top: 8px;
130
+ }
131
+ .mms-media img {
132
+ max-width: 100%;
133
+ border-radius: 12px;
134
+ border: 1px solid var(--border);
135
+ }
136
+ .mms-media a {
137
+ font-size: 12px;
138
+ color: var(--accent);
139
+ }
140
+ /* Inspector panel */
141
+ .inspector {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 16px;
145
+ }
146
+ .panel {
147
+ background: var(--card-bg);
148
+ border: 1px solid var(--border);
149
+ border-radius: 14px;
150
+ padding: 18px;
151
+ }
152
+ .panel-title {
153
+ font-size: 12px;
154
+ font-weight: 700;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.06em;
157
+ color: var(--muted);
158
+ margin: 0 0 12px 0;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: space-between;
162
+ }
163
+ .badge {
164
+ display: inline-block;
165
+ padding: 3px 9px;
166
+ border-radius: 999px;
167
+ font-size: 11px;
168
+ font-weight: 600;
169
+ background: rgba(56, 189, 248, 0.15);
170
+ color: var(--accent);
171
+ }
172
+ .badge.warning {
173
+ background: rgba(245, 158, 11, 0.18);
174
+ color: var(--warning);
175
+ }
176
+ .badge.success {
177
+ background: rgba(16, 185, 129, 0.18);
178
+ color: var(--success);
179
+ }
180
+ .otp-banner {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ background: rgba(16, 185, 129, 0.12);
185
+ border: 1px solid rgba(16, 185, 129, 0.4);
186
+ border-radius: 12px;
187
+ padding: 14px 16px;
188
+ }
189
+ .otp-code-value {
190
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
191
+ font-size: 22px;
192
+ font-weight: 700;
193
+ letter-spacing: 0.12em;
194
+ color: #34d399;
195
+ }
196
+ .btn {
197
+ cursor: pointer;
198
+ background: #10b981;
199
+ color: #06281e;
200
+ border: none;
201
+ border-radius: 8px;
202
+ padding: 8px 14px;
203
+ font-size: 13px;
204
+ font-weight: 700;
205
+ transition: opacity 0.15s;
206
+ }
207
+ .btn:hover { opacity: 0.9; }
208
+ .meta-grid {
209
+ display: grid;
210
+ grid-template-columns: 120px 1fr;
211
+ row-gap: 10px;
212
+ column-gap: 12px;
213
+ font-size: 13.5px;
214
+ }
215
+ .meta-label { color: var(--muted); font-weight: 500; }
216
+ .meta-value {
217
+ color: var(--text);
218
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
219
+ word-break: break-all;
220
+ }
221
+ .links-list {
222
+ list-style: none;
223
+ padding: 0;
224
+ margin: 0;
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 8px;
228
+ }
229
+ .links-list a {
230
+ display: block;
231
+ padding: 8px 12px;
232
+ background: rgba(15, 23, 42, 0.6);
233
+ border: 1px solid var(--border);
234
+ border-radius: 8px;
235
+ color: var(--accent);
236
+ text-decoration: none;
237
+ font-size: 13px;
238
+ word-break: break-all;
239
+ }
240
+ .links-list a:hover {
241
+ border-color: var(--accent);
242
+ }
243
+ </style>
244
+ </head>
245
+ <body>
246
+ <div class="container">
247
+ <!-- Mobile Device Preview -->
248
+ <div class="phone-frame">
249
+ <div class="phone-screen">
250
+ <div class="phone-statusbar">
251
+ <span><%= created_at.strftime("%H:%M") %></span>
252
+ <span>5G • 100%</span>
253
+ </div>
254
+ <div class="phone-header">
255
+ <div class="avatar-circle">SMS</div>
256
+ <div class="sender-label"><%= CGI.escapeHTML(from) %></div>
257
+ <div class="recipient-sub">To: <%= CGI.escapeHTML(to_display) %></div>
258
+ </div>
259
+ <div class="chat-area">
260
+ <div class="chat-timestamp"><%= created_at.strftime("%b %d, %Y at %I:%M:%S %p") %></div>
261
+ <div class="sms-bubble" id="sms-body"><%= formatted_body_html %></div>
262
+ <% unless media_urls.empty? -%>
263
+ <div class="mms-media">
264
+ <% media_urls.each do |url| -%>
265
+ <img src="<%= CGI.escapeHTML(url) %>" alt="MMS Attachment" onerror="this.style.display='none'">
266
+ <a href="<%= CGI.escapeHTML(url) %>" target="_blank" rel="noopener noreferrer"><%= CGI.escapeHTML(url) %></a>
267
+ <% end -%>
268
+ </div>
269
+ <% end -%>
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ <!-- Developer Metadata & Quick Actions -->
275
+ <div class="inspector">
276
+ <% if otp_code -%>
277
+ <div class="otp-banner">
278
+ <div>
279
+ <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #a7f3d0; margin-bottom: 2px;">Detected Verification Code</div>
280
+ <div class="otp-code-value" id="otp-code"><%= CGI.escapeHTML(otp_code) %></div>
281
+ </div>
282
+ <button class="btn" type="button" onclick="navigator.clipboard.writeText('<%= CGI.escapeHTML(otp_code) %>'); this.textContent = 'Copied!'; setTimeout(() => this.textContent = 'Copy OTP', 1500);">Copy OTP</button>
283
+ </div>
284
+ <% end -%>
285
+
286
+ <div class="panel">
287
+ <div class="panel-title">
288
+ <span>Delivery Details</span>
289
+ <span class="badge"><%= CGI.escapeHTML(provider.upcase) %></span>
290
+ </div>
291
+ <div class="meta-grid">
292
+ <div class="meta-label">Message ID</div>
293
+ <div class="meta-value"><%= CGI.escapeHTML(id) %></div>
294
+
295
+ <div class="meta-label">From</div>
296
+ <div class="meta-value"><%= CGI.escapeHTML(from) %></div>
297
+
298
+ <div class="meta-label">To</div>
299
+ <div class="meta-value"><%= CGI.escapeHTML(to_display) %></div>
300
+
301
+ <div class="meta-label">Timestamp</div>
302
+ <div class="meta-value"><%= CGI.escapeHTML(created_at.iso8601) %></div>
303
+ </div>
304
+ </div>
305
+
306
+ <div class="panel">
307
+ <div class="panel-title">
308
+ <span>SMS Segment &amp; Encoding Analysis</span>
309
+ <span class="badge <%= segments > 1 || encoding == 'UCS-2' ? 'warning' : 'success' %>">
310
+ <%= segments %> <%= segments == 1 ? 'Segment' : 'Segments' %> (<%= encoding %>)
311
+ </span>
312
+ </div>
313
+ <div class="meta-grid">
314
+ <div class="meta-label">Encoding</div>
315
+ <div class="meta-value"><%= encoding %></div>
316
+
317
+ <div class="meta-label">Characters</div>
318
+ <div class="meta-value"><%= character_count %></div>
319
+
320
+ <div class="meta-label">Segments</div>
321
+ <div class="meta-value"><%= segments %></div>
322
+ </div>
323
+ </div>
324
+
325
+ <% unless urls.empty? -%>
326
+ <div class="panel">
327
+ <div class="panel-title"><span>Extracted Links (<%= urls.size %>)</span></div>
328
+ <ul class="links-list">
329
+ <% urls.each do |url| -%>
330
+ <li><a href="<%= CGI.escapeHTML(url) %>" target="_blank" rel="noopener noreferrer"><%= CGI.escapeHTML(url) %></a></li>
331
+ <% end -%>
332
+ </ul>
333
+ </div>
334
+ <% end -%>
335
+
336
+ <% unless metadata.empty? -%>
337
+ <div class="panel">
338
+ <div class="panel-title"><span>Provider Metadata</span></div>
339
+ <div class="meta-grid">
340
+ <% metadata.each do |key, val| -%>
341
+ <div class="meta-label"><%= CGI.escapeHTML(key.to_s) %></div>
342
+ <div class="meta-value"><%= CGI.escapeHTML(val.to_s) %></div>
343
+ <% end -%>
344
+ </div>
345
+ </div>
346
+ <% end -%>
347
+ </div>
348
+ </div>
349
+ </body>
350
+ </html>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmsOpener
4
+ VERSION = "0.1.1"
5
+ end
data/lib/sms_opener.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sms_opener/version"
4
+ require_relative "sms_opener/configuration"
5
+ require_relative "sms_opener/message"
6
+ require_relative "sms_opener/delivery_method"
7
+
8
+ module SmsOpener
9
+ class << self
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def configure
15
+ yield(configuration) if block_given?
16
+ if configuration.intercept_twilio && defined?(SmsOpener::Interceptors::TwilioInterceptor)
17
+ SmsOpener::Interceptors::TwilioInterceptor.install!
18
+ end
19
+ configuration
20
+ end
21
+
22
+ def reset_configuration!
23
+ @configuration = Configuration.new
24
+ end
25
+
26
+ def deliver(message_or_attributes = nil, **kwargs)
27
+ DeliveryMethod.new.deliver!(message_or_attributes, **kwargs)
28
+ end
29
+
30
+ def messages
31
+ Message.all
32
+ end
33
+
34
+ def find(id)
35
+ Message.find(id)
36
+ end
37
+
38
+ def clear!
39
+ Message.clear!
40
+ end
41
+ end
42
+ end
43
+
44
+ require_relative "sms_opener/interceptors/twilio_interceptor" if File.exist?(File.expand_path("sms_opener/interceptors/twilio_interceptor.rb", __dir__))
45
+ require_relative "sms_opener/railtie" if defined?(Rails::Railtie) && File.exist?(File.expand_path("sms_opener/railtie.rb", __dir__))
46
+ require_relative "sms_opener/engine" if defined?(Rails::Engine) && File.exist?(File.expand_path("sms_opener/engine.rb", __dir__))
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sms_opener
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott Stubblebine
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: launchy
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: actionpack
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9.0'
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '7.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '7.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '9.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '7.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '9.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: rack-test
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '2.0'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '3.0'
93
+ - !ruby/object:Gem::Dependency
94
+ name: railties
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '7.0'
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: '9.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '7.0'
110
+ - - "<"
111
+ - !ruby/object:Gem::Version
112
+ version: '9.0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: rake
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '13.0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '13.0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rspec
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '3.13'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '3.13'
141
+ - !ruby/object:Gem::Dependency
142
+ name: twilio-ruby
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '6.0'
148
+ - - "<"
149
+ - !ruby/object:Gem::Version
150
+ version: '8.0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '6.0'
158
+ - - "<"
159
+ - !ruby/object:Gem::Version
160
+ version: '8.0'
161
+ description: Inspired by letter_opener and letter_opener_web, sms_opener intercepts
162
+ outgoing SMS/MMS messages (including Twilio and custom SMS adapters) in development,
163
+ opens a mobile preview in your browser via Launchy, and provides a mountable Rails
164
+ Engine web inbox.
165
+ email:
166
+ - scott.c.stubblebine@gmail.com
167
+ executables: []
168
+ extensions: []
169
+ extra_rdoc_files: []
170
+ files:
171
+ - LICENSE.txt
172
+ - README.md
173
+ - Rakefile
174
+ - app/controllers/sms_opener/messages_controller.rb
175
+ - app/views/sms_opener/messages/index.html.erb
176
+ - config/routes.rb
177
+ - lib/sms_opener.rb
178
+ - lib/sms_opener/configuration.rb
179
+ - lib/sms_opener/delivery_method.rb
180
+ - lib/sms_opener/engine.rb
181
+ - lib/sms_opener/interceptors/twilio_interceptor.rb
182
+ - lib/sms_opener/message.rb
183
+ - lib/sms_opener/railtie.rb
184
+ - lib/sms_opener/templates/sms.html.erb
185
+ - lib/sms_opener/version.rb
186
+ homepage: https://github.com/stubblsc/sms_opener
187
+ licenses:
188
+ - MIT
189
+ metadata:
190
+ homepage_uri: https://github.com/stubblsc/sms_opener
191
+ source_code_uri: https://github.com/stubblsc/sms_opener
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: 3.0.0
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubygems_version: 3.5.22
208
+ signing_key:
209
+ specification_version: 4
210
+ summary: Preview SMS and MMS messages in the browser instead of sending them in Ruby/Rails
211
+ development.
212
+ test_files: []