testimonials 0.9.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cbc7f22314c712ecb0e1eb5d92a3d66981193bc1fc19f8664f635d381db0045
4
- data.tar.gz: '020981c67a45702104ebf9cdfd9f685ffc17d44653a3476560c0fb662176e445'
3
+ metadata.gz: d114855497d2d5186be09b6e76bab5f0d78e079012cfb765dd2399b61271e191
4
+ data.tar.gz: 0c3c906e8dd2aa883cf0261565b2c4f118c0ab8825c06683cbce18f4e59e3a17
5
5
  SHA512:
6
- metadata.gz: 1ca4a49fd102d401ae88b344c951655e68e88af3fb0da1a11819b7b600edb9ad11543feb5440a25f2127e138f4da54d9d7447a3b260a79d7e3cb59b5abcdbfb2
7
- data.tar.gz: d8c94fe02b56b3e91adf3eb39e1c87e88b70d4f0d9ed4102e8058901b019f0a1c5fb4d1d02327be255c18b5ca58afedae06e7386efd58978d16d017fde1bc8d5
6
+ metadata.gz: b9a26a9dd5f656c84cf657ed34bda8f130d3783be1efc98987d8bf438abb25728b6c34dc8c62b28e6e39a65a9a117baf411542da563ddfe9549b96edf1e545a9
7
+ data.tar.gz: de494a843ba1747d54a500815eaa2e212b99bc66b5302f6e59aed64f0ea7afba73a1257af1967a5052e441db5a59f6ba56ec9cb578938e18e91450f4d761f88d
data/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ Instructions for coding agents. Two audiences:
5
5
  - **[Installing testimonials into a Rails app](#installing-into-a-rails-app)** — you are working in a host app and were asked to add testimonials, reviews, or NPS.
6
6
  - **[Working on the gem itself](#working-on-the-gem-itself)** — you are working in this repository.
7
7
 
8
- Requirements: Ruby >= 3.2, Rails >= 7.1. Active Storage only for video and avatar uploads.
8
+ Requirements: Ruby >= 3.2, Rails >= 7.1 and < 9. Active Storage only for video and avatar uploads.
9
9
 
10
10
  If you are in a host app and this file is not in front of you, it ships inside the gem: `cat "$(bundle show testimonials)/AGENTS.md"`.
11
11
 
@@ -196,8 +196,14 @@ Turbo Drive and nonce-based CSP work out of the box; the widget config rides in
196
196
 
197
197
  ## Working on the gem itself
198
198
 
199
+ The development toolchain is pinned to Ruby 4.0.5 in `mise.toml`. Do not use
200
+ macOS's `/usr/bin/ruby`. In shells, hooks, or automation where mise activation
201
+ is uncertain, explicitly prepend the configured Ruby, for example
202
+ `PATH="$(mise where ruby)/bin:$PATH" bundle exec rake test`.
203
+
199
204
  ```bash
200
205
  bundle exec rake test # minitest, dummy app under test/dummy
206
+ bundle exec rake test:system # real Chrome: NPS, testimonial, moderation, public read
201
207
  bundle exec rubocop # must be clean
202
208
  BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test # 7.1, 7.2, 8.0, 8.1 in gemfiles/
203
209
  ```
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 - 2026-08-11
4
+
5
+ - **The 1.x record names are settled.** `Testimonials::Testimonial`,
6
+ `Testimonials::NpsResponse`, and `Testimonials::PromptEvent` are now
7
+ documented as the stable model constants rather than being hidden behind a
8
+ generic `Post` name. Their existing optional-install boundaries are
9
+ unchanged.
10
+ - **A real Chrome test now proves the revenue-adjacent path:** an NPS promoter
11
+ is offered the testimonial form, submits text with public consent, lands in
12
+ pending moderation, is approved, and appears through the public read API
13
+ without leaking email. Capybara and Selenium are development/test
14
+ dependencies only.
15
+ - **Uploaded videos, posters, and avatars stay behind the engine gate.** Media
16
+ responses now stream full and byte-range data directly after authorization
17
+ instead of redirecting to a reusable signed Active Storage URL. Code that
18
+ inspected a media response's `Location` header must consume the engine URL
19
+ directly; browser `<video>` and image URLs are unchanged.
20
+ - **Hosts that load Action View from an initializer boot normally.** The widget
21
+ helper is loaded from `lib` before the engine registers its Action View hook;
22
+ `testimonials_tag` and the opener helpers are unchanged.
23
+ - Added a security policy documenting stored testimonial, NPS, prompt-event,
24
+ consent, media, deletion, and retention boundaries. GitHub private
25
+ vulnerability reporting is the disclosure channel. Submitted page URLs now
26
+ accept only HTTP(S) and drop query strings/fragments before storage, keeping
27
+ tokens out of the database and executable schemes out of dashboard links.
28
+ - Supported combinations are Ruby 3.2 through 4.0 and Rails 7.1 through 8.1;
29
+ Rails now has a `< 9` upper bound. Release validation runs the browser test,
30
+ lint and JavaScript checks, builds the gem, verifies RubyGems indexing, and
31
+ creates the GitHub Release.
32
+ - Existing 0.x installs need no migration, initializer change, constant alias,
33
+ or model rename. The new Rails upper bound and non-redirecting media response
34
+ are the only compatibility costs.
35
+
3
36
  ## 0.9.1
4
37
 
5
38
  - **The demo data now teaches responsible collection.** Its testimonials cover
data/README.md CHANGED
@@ -66,7 +66,8 @@ refreshes those records instead of duplicating them.
66
66
  > The dashboard defaults to **development only**. Set `authorize_admin` before
67
67
  > you deploy — see [Configure](#configure).
68
68
 
69
- Ruby >= 3.2 · Rails >= 7.1 · Active Storage only if you want video/avatar uploads.
69
+ Ruby >= 3.2 · Rails >= 7.1 and < 9 · Active Storage only if you want
70
+ video/avatar uploads.
70
71
 
71
72
  Installing with a coding agent? Point it at [AGENTS.md](AGENTS.md) — the same
72
73
  steps in the order an agent needs them, plus the gates it tends to get wrong
@@ -240,7 +241,7 @@ playback, feature toggle, and a **Best line** picker.
240
241
  <img src="https://raw.githubusercontent.com/yshmarov/testimonials/main/docs/screenshots/dashboard-show.jpg" alt="A single testimonial in the dashboard: video playback, best-line picker, attribution metadata, and approve/archive/feature/delete actions" width="620">
241
242
 
242
243
  Every submission keeps its provenance — who sent it, the exact consent text
243
- they agreed to, the page, locale, browser, timestamp. **Best line** is the only
244
+ they agreed to, the query-free page URL, locale, browser, timestamp. **Best line** is the only
244
245
  thing you write: the customer's words stay verbatim, you just choose which
245
246
  sentence your landing page pulls out.
246
247
 
@@ -255,6 +256,8 @@ Admin-only by default. `config.public_api = true` serves them without auth
255
256
  (CORS `*`) — ideal for a static marketing site rendering your wall of love at
256
257
  build time. **Emails and author ids are never serialized.** Video and avatar
257
258
  files go through the same gate, with Range support so `<video>` just works.
259
+ The media response stays on that authorized engine URL with a private,
260
+ `no-store` cache policy; it never redirects to a reusable signed blob URL.
258
261
 
259
262
  ## Rendering what you collect
260
263
 
@@ -358,6 +361,49 @@ collection — so nothing changes until you set `config.tenant`.
358
361
 
359
362
  </details>
360
363
 
364
+ ## Compatibility and public API
365
+
366
+ The following are the model and integration contracts that 1.x will keep
367
+ stable under semantic versioning:
368
+
369
+ - `Testimonials::Testimonial`, including `KINDS`, `STATUSES`, `SOURCES`, its
370
+ persisted fields, attachment predicates, status predicates, `for_tenant`,
371
+ `newest_first`, `approved`, `publishable`, `featured_first`, `display_name`,
372
+ and `quote`.
373
+ - `Testimonials::NpsResponse`, including its persisted fields, `for_tenant`,
374
+ `newest_first`, `promoter?`, `passive?`, `detractor?`, `.score`, and `.band`
375
+ when NPS is installed.
376
+ - `Testimonials::PromptEvent`, including `KINDS`, `ACTIONS`, its persisted
377
+ fields, `.eligible?`, and `.record!` when prompt events are installed.
378
+ - `has_testimonials`, `Testimonials.configure`, `mount_testimonials`,
379
+ `testimonials_tag`, the opener helpers/attributes, and `testimonial_prompt!`.
380
+ - `config.on_submit` and `config.on_detractor`, each receiving the documented
381
+ saved record.
382
+
383
+ Those domain model names are intentional and will not become a generic `Post`
384
+ during 1.x. Engine controllers, partials, CSS classes, generated HTML, and
385
+ widget implementation objects are private. Incompatible changes to the public
386
+ list above wait for a new major version; a deprecation normally ships first.
387
+
388
+ ### Upgrading from 0.x
389
+
390
+ Version 1.0.0 keeps the existing domain models and schema. There is no
391
+ migration, initializer change, compatibility alias, or rename: continue using
392
+ `Testimonials::Testimonial`, `Testimonials::NpsResponse`, and
393
+ `Testimonials::PromptEvent`. Rails 9 is excluded until a 1.x release explicitly
394
+ adds it. Media URLs are unchanged, but their responses now contain the gated
395
+ bytes instead of redirecting to an Active Storage URL.
396
+
397
+ ## Security and deletion
398
+
399
+ See [SECURITY.md](https://github.com/yshmarov/testimonials/blob/main/SECURITY.md)
400
+ for private vulnerability reporting and the stored-data, consent, media,
401
+ deletion, and retention boundaries. In short: the public API is off by default;
402
+ when enabled it returns only approved, publicly consented records without email
403
+ or author ids. Deleting the live testimonial, NPS, and prompt-event rows is the
404
+ host's responsibility, as are object-store, backup, analytics, cache, and export
405
+ retention policies.
406
+
361
407
  ## Who's using it
362
408
 
363
409
  - [PaidCollabs](https://paidcollabs.com) — the screenshots above
@@ -369,9 +415,17 @@ add yourself.
369
415
 
370
416
  ```bash
371
417
  bundle exec rake test
418
+ bundle exec rake test:system
372
419
  bundle exec rubocop
420
+ node --check lib/testimonials/widget.js
421
+ node --check lib/testimonials/dashboard.js
373
422
  ```
374
423
 
424
+ The default suite covers the Rails engine contract. The system task drives a
425
+ real headless Chrome session from NPS through promoter handoff, testimonial
426
+ submission, moderation, and public read. CI runs Rails 7.1, 7.2, 8.0, and 8.1
427
+ against Ruby 3.2 through 4.0.
428
+
375
429
  Bug reports and pull requests welcome. The fastest way to help is to install it
376
430
  in a real app and
377
431
  [tell me where it hurt](https://github.com/yshmarov/testimonials/issues).
data/Rakefile CHANGED
@@ -5,7 +5,15 @@ require 'rake/testtask'
5
5
 
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << 'test'
8
- t.pattern = 'test/**/*_test.rb'
8
+ t.test_files = FileList['test/**/*_test.rb'].exclude('test/system/**/*')
9
+ end
10
+
11
+ namespace :test do
12
+ desc 'Run browser (system) tests'
13
+ Rake::TestTask.new(:system) do |task|
14
+ task.libs << 'test'
15
+ task.test_files = FileList['test/system/**/*_test.rb']
16
+ end
9
17
  end
10
18
 
11
19
  task default: :test
data/SECURITY.md ADDED
@@ -0,0 +1,57 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are released for the latest 1.x version. If a report also
6
+ affects the newest 0.x release, a backport may be published when the fix is
7
+ small and the affected application cannot upgrade immediately.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Please do not open a public issue for a vulnerability. Use
12
+ [GitHub private vulnerability reporting](https://github.com/yshmarov/testimonials/security/advisories/new)
13
+ and include the affected version, a minimal reproduction, impact, and any known
14
+ workaround. Do not include credentials, session cookies, private application
15
+ URLs, customer submissions, uploaded media, or production database contents.
16
+
17
+ Public disclosure should wait until a fixed version is available and affected
18
+ users have had a reasonable opportunity to upgrade.
19
+
20
+ ## Data and deployment boundary
21
+
22
+ The public collection pages and widget are controlled by `config.enabled`;
23
+ the dashboard is separately protected by `authorize_admin` and fails closed
24
+ outside development until the host grants access. `config.public_api` is off by
25
+ default. When enabled, it serves only approved testimonials carrying public
26
+ consent and never serializes email or `author_id`.
27
+
28
+ Testimonials can store their text, rating, best line, moderation status,
29
+ consent choice and exact consent-text snapshot, attribution, source, page URL,
30
+ user agent, locale, tenant key, timestamps, and optional video, poster, and
31
+ avatar uploads. Page URLs are accepted only as HTTP(S) and stored without query
32
+ strings or fragments. NPS responses can store the score, comment, attribution,
33
+ the same query-free page URL, user agent, locale, tenant key, and timestamps.
34
+ Prompt events store the prompt kind/action, author id or pseudonymous visitor
35
+ token, tenant key, and timestamps.
36
+
37
+ Uploaded media streams through the engine's authorization gate with private,
38
+ non-stored responses; the route never redirects to a reusable signed blob URL.
39
+ Hosts own the storage service, bucket policy, backups, retention periods,
40
+ access controls, and incident response. Enabling the public API deliberately
41
+ makes approved, publicly consented testimonial media readable to anyone who
42
+ has its engine URL.
43
+
44
+ Delete records when an application policy or customer request requires it:
45
+
46
+ ```ruby
47
+ Testimonials::Testimonial.find(id).destroy!
48
+ Testimonials::NpsResponse.find(id).destroy!
49
+ Testimonials::PromptEvent.where(author_id: user.id.to_s).delete_all
50
+ ```
51
+
52
+ Destroying a testimonial lets Rails apply its configured Active Storage
53
+ attachment lifecycle. Confirm deletion separately in object storage, replicas,
54
+ backups, caches, analytics, notifications, and exports; deleting live database
55
+ rows does not rewrite those systems. Consent is evidence of the choice made at
56
+ submission time, not a substitute for the host's withdrawal and retention
57
+ process.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module Testimonials
4
6
  # Who is asking, which tenant they are in, and the gates that answer both.
5
7
  #
@@ -41,6 +43,21 @@ module Testimonials
41
43
  Testimonial.for_tenant(current_tenant)
42
44
  end
43
45
 
46
+ # Browser URLs can carry password-reset tokens, signed ids, and campaign
47
+ # details in their query or fragment. Store only a safe HTTP(S) page origin
48
+ # and path; invalid or executable schemes become nil before the dashboard
49
+ # ever renders them as links.
50
+ def clean_page_url(value)
51
+ uri = URI.parse(value.to_s)
52
+ return unless uri.is_a?(URI::HTTP) && uri.host.present? && uri.userinfo.nil?
53
+
54
+ uri.query = nil
55
+ uri.fragment = nil
56
+ uri.to_s
57
+ rescue URI::InvalidURIError
58
+ nil
59
+ end
60
+
44
61
  def require_enabled
45
62
  head :forbidden unless Testimonials.enabled?(request)
46
63
  end
@@ -5,34 +5,50 @@ module Testimonials
5
5
  # need to know about Active Storage. Admins can fetch anything; everyone
6
6
  # else only reaches media of publishable (approved + consented)
7
7
  # testimonials, and only when the read API is public. The actual bytes are
8
- # served by Active Storage's own controllers (signed, expiring URLs, with
9
- # Range support video elements need that).
8
+ # streamed here after authorization. Keeping the browser on this route means
9
+ # an Active Storage URL cannot outlive the gate; Range support remains for
10
+ # video playback and seeking.
10
11
  class MediaController < ApplicationController
12
+ include ActiveStorage::Streaming if defined?(::ActiveStorage::Streaming)
13
+
11
14
  before_action :set_testimonial
12
15
  before_action :authorize_media
13
16
 
14
17
  def video
15
18
  head :not_found and return unless @testimonial.video_attached?
16
19
 
17
- redirect_to main_app.rails_blob_path(@testimonial.video_file, disposition: disposition)
20
+ stream(@testimonial.video_file)
18
21
  end
19
22
 
20
23
  def poster
21
24
  head :not_found and return unless @testimonial.poster_attached?
22
25
 
23
- redirect_to main_app.rails_blob_path(@testimonial.poster, disposition: disposition)
26
+ stream(@testimonial.poster)
24
27
  end
25
28
 
26
29
  def avatar
27
30
  head :not_found and return unless @testimonial.avatar_attached?
28
31
 
29
- redirect_to main_app.rails_blob_path(@testimonial.avatar, disposition: disposition)
32
+ stream(@testimonial.avatar)
30
33
  end
31
34
 
32
35
  private
33
36
 
34
- # ?download=1 turns the redirect into a file download instead of
35
- # inline playback.
37
+ def stream(attachment)
38
+ blob = attachment.blob
39
+ response.headers['Cache-Control'] = 'private, no-store'
40
+ if request.headers['Range'].present?
41
+ send_blob_byte_range_data(blob, request.headers['Range'], disposition: disposition)
42
+ else
43
+ response.headers['Accept-Ranges'] = 'bytes'
44
+ response.headers['Content-Length'] = blob.byte_size.to_s
45
+ send_blob_stream(blob, disposition: disposition)
46
+ end
47
+ end
48
+
49
+ # ?download=1 explicitly requests a file download. Without it we request
50
+ # inline playback; Active Storage may still force attachment disposition
51
+ # for content types outside the host's safe-inline allowlist.
36
52
  def disposition
37
53
  params[:download].present? ? 'attachment' : 'inline'
38
54
  end
@@ -48,7 +64,11 @@ module Testimonials
48
64
  # Authors can always see their own media — the widget replays their
49
65
  # attached video when they edit their review.
50
66
  return if current_author_id.present? && @testimonial.author_id == current_author_id.to_s
51
- return if Testimonials.config.public_api && @testimonial.approved? && @testimonial.consent_given?
67
+
68
+ if Testimonials.config.public_api && @testimonial.approved? && @testimonial.consent_given?
69
+ response.set_header('Access-Control-Allow-Origin', '*')
70
+ return
71
+ end
52
72
 
53
73
  head :forbidden
54
74
  end
@@ -13,6 +13,7 @@ module Testimonials
13
13
  nps = NpsResponse.new(nps_params)
14
14
  nps.locale = I18n.locale.to_s
15
15
  nps.tenant = current_tenant
16
+ nps.page_url = clean_page_url(nps.page_url)
16
17
  nps.user_agent = request.user_agent
17
18
  attribute_author(nps)
18
19
 
@@ -55,6 +55,7 @@ module Testimonials
55
55
  end
56
56
  testimonial.kind = wants_video?(testimonial) ? 'video' : 'text'
57
57
  testimonial.source = 'widget' unless Testimonial::SOURCES.include?(testimonial.source)
58
+ testimonial.page_url = clean_page_url(testimonial.page_url)
58
59
  testimonial.consent_text = Testimonials.consent_text_for(testimonial.consent_given?)
59
60
  testimonial.locale = I18n.locale.to_s
60
61
  testimonial.tenant = current_tenant
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.9.1'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -1,15 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- # Included into the host's ActionView. Drop `<%= testimonials_tag %>`
5
- # before </body> in your layout; it renders nothing unless reviews are
6
- # enabled for the request. The widget stays invisible until opened by a
7
- # `testimonial_prompt!` from a controller (throttled), by any element with a
8
- # `data-testimonial-prompt` attribute, or by `window.Testimonials.open()`.
9
- #
10
- # `testimonials_button` renders a ready-made opener with a localized
11
- # label ("Leave a review", or "Update your review" once the user has one),
12
- # so hosts get a user-facing entry point without any i18n setup.
4
+ # Included into the host's ActionView. This lives under lib and is required
5
+ # before the engine registers its Action View load hook: a host initializer
6
+ # may load Action View before Rails sets up application autoloaders, when an
7
+ # app/helpers constant cannot be resolved yet.
13
8
  module WidgetHelper
14
9
  def testimonials_tag
15
10
  return unless Testimonials.enabled?(request)
@@ -23,19 +18,12 @@ module Testimonials
23
18
  ).html_safe
24
19
  end
25
20
 
26
- # A plain, unstyled <button> so it picks up the host's own styles. Put it
27
- # anywhere on a page that also renders testimonials_tag. Pass label: to
28
- # override the localized default, and any other options (class:, etc.)
29
- # through to the tag.
30
21
  def testimonials_button(label: nil, **)
31
22
  return unless Testimonials.enabled?(request)
32
23
 
33
24
  tag.button(label || testimonials_cta_label, type: 'button', 'data-testimonial-prompt': '', **)
34
25
  end
35
26
 
36
- # The localized call-to-action on its own — "Leave a review", or "Update
37
- # your review" once the user has one — for hosts composing their own
38
- # opener markup (icons, list items) around a data-testimonial-prompt element.
39
27
  def testimonials_cta_label
40
28
  if testimonials_existing_testimonial
41
29
  I18n.t(:update_title, scope: :testimonials, default: 'Update your review')
@@ -52,8 +40,6 @@ module Testimonials
52
40
  @testimonials_author = Testimonials.config.current_user.call(request)
53
41
  end
54
42
 
55
- # One review per signed-in user (per tenant): this is the record the widget
56
- # opens pre-filled, and the one the create endpoint updates in place.
57
43
  def testimonials_existing_testimonial
58
44
  return @testimonials_existing_testimonial if defined?(@testimonials_existing_testimonial)
59
45
 
@@ -65,16 +51,10 @@ module Testimonials
65
51
  end
66
52
  end
67
53
 
68
- # A prompt requested via testimonial_prompt! rides the flash, so it survives
69
- # the usual redirect-after-create. It only reaches the page if the
70
- # throttle ledger agrees — call testimonial_prompt! as often as you like.
71
54
  def testimonials_auto_open
72
55
  kind = flash[:testimonials_prompt].to_s
73
56
  return unless Testimonials::PromptEvent::KINDS.include?(kind)
74
57
  return if kind == 'nps' && !Testimonials.config.nps
75
- # Without the ledger there is no record of who has been asked already, so
76
- # an auto-open would reopen on every page a testimonial_prompt! reaches.
77
- # An install with --skip-prompt-events opens on a click and nothing else.
78
58
  return unless Testimonials.config.prompt_events
79
59
 
80
60
  author = testimonials_author
data/lib/testimonials.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'testimonials/version'
4
4
  require 'testimonials/configuration'
5
5
  require 'testimonials/widget'
6
+ require 'testimonials/widget_helper'
6
7
  require 'testimonials/prompt_helper'
7
8
  require 'testimonials/has_testimonials'
8
9
  require 'testimonials/seeds'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
26
32
  description: |
27
33
  A mountable Rails engine that collects customer testimonials — text and
28
34
  video — and NPS scores from inside your app. An iOS-style "Enjoying this
@@ -43,6 +49,7 @@ files:
43
49
  - MIT-LICENSE
44
50
  - README.md
45
51
  - Rakefile
52
+ - SECURITY.md
46
53
  - app/controllers/concerns/testimonials/request_context.rb
47
54
  - app/controllers/testimonials/api/base_controller.rb
48
55
  - app/controllers/testimonials/api/stats_controller.rb
@@ -57,7 +64,6 @@ files:
57
64
  - app/controllers/testimonials/submissions_controller.rb
58
65
  - app/controllers/testimonials/testimonials_controller.rb
59
66
  - app/controllers/testimonials/widgets_controller.rb
60
- - app/helpers/testimonials/widget_helper.rb
61
67
  - app/models/testimonials/application_record.rb
62
68
  - app/models/testimonials/nps_response.rb
63
69
  - app/models/testimonials/prompt_event.rb
@@ -129,12 +135,13 @@ files:
129
135
  - lib/testimonials/version.rb
130
136
  - lib/testimonials/widget.js
131
137
  - lib/testimonials/widget.rb
138
+ - lib/testimonials/widget_helper.rb
132
139
  homepage: https://github.com/yshmarov/testimonials
133
140
  licenses:
134
141
  - MIT
135
142
  metadata:
136
143
  homepage_uri: https://github.com/yshmarov/testimonials
137
- source_code_uri: https://github.com/yshmarov/testimonials
144
+ source_code_uri: https://github.com/yshmarov/testimonials/tree/main
138
145
  changelog_uri: https://github.com/yshmarov/testimonials/blob/main/CHANGELOG.md
139
146
  bug_tracker_uri: https://github.com/yshmarov/testimonials/issues
140
147
  rubygems_mfa_required: 'true'
@@ -152,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
159
  - !ruby/object:Gem::Version
153
160
  version: '0'
154
161
  requirements: []
155
- rubygems_version: 4.0.10
162
+ rubygems_version: 4.0.16
156
163
  specification_version: 4
157
164
  summary: 'Testimonials, reviews and NPS for Rails: in-app collection widget, public
158
165
  collection page, triage dashboard, and a read API.'