diff --git a/app/components/live_release/metadata_component.html.erb b/app/components/live_release/metadata_component.html.erb
index 621468616..e47965d04 100644
--- a/app/components/live_release/metadata_component.html.erb
+++ b/app/components/live_release/metadata_component.html.erb
@@ -13,24 +13,24 @@
<%= render ButtonComponent.new(label: "Play Store metadata requirements",
- scheme: :link,
- type: :link_external,
- options: "https://play.google.com/console/about/storelistings/#best-practices",
- html_options: { class: "text-sm" },
- authz: false,
- size: :none,
- arrow: :none) do |b|
+ scheme: :link,
+ type: :link_external,
+ options: "https://play.google.com/console/about/storelistings/#best-practices",
+ html_options: { class: "text-sm" },
+ authz: false,
+ size: :none,
+ arrow: :none) do |b|
b.with_icon("integrations/logo_google_play_store.png", size: :md, classes: "text-main")
end %>
<%= render ButtonComponent.new(label: "App Store Connect metadata requirements",
- scheme: :link,
- type: :link_external,
- options: "https://help.apple.com/asc/appsspec/en.lproj/static.html",
- html_options: { class: "text-sm" },
- authz: false,
- size: :none,
- arrow: :none) do |b|
+ scheme: :link,
+ type: :link_external,
+ options: "https://help.apple.com/asc/appsspec/en.lproj/static.html",
+ html_options: { class: "text-sm" },
+ authz: false,
+ size: :none,
+ arrow: :none) do |b|
b.with_icon("integrations/logo_app_store.png", size: :md, classes: "text-main")
end %>
@@ -81,7 +81,11 @@
<% f.F.fields_for :android, android_metadata do |aF| %>
<%= aF.hidden_field :id %>
-
<%= aF.labeled_textarea :release_notes, "Release Notes" %>
+ <%= render partial: "shared/size_limited_textarea", locals: { form: aF,
+ obj_method: :release_notes,
+ label_text: "Release Notes",
+ max_length: android_max_length,
+ existing_value: android_metadata.release_notes } %>
<% end %>
<% elsif component.cross_platform? %>
@@ -94,8 +98,16 @@
<% f.F.fields_for :ios, ios_metadata do |iosF| %>
<%= iosF.hidden_field :id %>
-
<%= iosF.labeled_textarea :release_notes, "What's New?" %>
-
<%= iosF.labeled_textarea :promo_text, "Promo Text" %>
+ <%= render partial: "shared/size_limited_textarea", locals: { form: iosF,
+ obj_method: :release_notes,
+ label_text: "Release Notes",
+ max_length: ios_max_length,
+ existing_value: ios_metadata.release_notes } %>
+ <%= render partial: "shared/size_limited_textarea", locals: { form: iosF,
+ obj_method: :promo_text,
+ label_text: "Promo Text",
+ max_length: promo_text_max_length,
+ existing_value: ios_metadata.promo_text } %>
<%= iosF.labeled_textarea :keywords, "Keywords", readonly: true, disabled: true %>
<%= iosF.labeled_textarea :description, "Description", readonly: true, disabled: true %>
diff --git a/app/components/live_release/metadata_component.rb b/app/components/live_release/metadata_component.rb
index 349e5b319..0df558169 100644
--- a/app/components/live_release/metadata_component.rb
+++ b/app/components/live_release/metadata_component.rb
@@ -27,4 +27,16 @@ def no_locale_set
def editable?
@release.release_platform_runs.any?(&:metadata_editable?)
end
+
+ def android_max_length
+ ReleaseMetadata::ANDROID_NOTES_MAX_LENGTH
+ end
+
+ def ios_max_length
+ ReleaseMetadata::IOS_NOTES_MAX_LENGTH
+ end
+
+ def promo_text_max_length
+ ReleaseMetadata::PROMO_TEXT_MAX_LENGTH
+ end
end
diff --git a/app/javascript/controllers/character_counter_controller.js b/app/javascript/controllers/character_counter_controller.js
new file mode 100644
index 000000000..a5c710b6d
--- /dev/null
+++ b/app/javascript/controllers/character_counter_controller.js
@@ -0,0 +1,26 @@
+import { Controller } from "@hotwired/stimulus"
+
+const ERROR_CLASS = "text-rose-700"
+
+export default class extends Controller {
+ static targets = ["input", "counter"]
+ static values = {
+ maxLength: {type: Number, default: 500},
+ }
+
+ connect() {
+ this.update()
+ }
+
+ update() {
+ const value = this.inputTarget.value.length
+
+ if (value > this.maxLengthValue) {
+ this.counterTarget.classList.add(ERROR_CLASS)
+ } else {
+ this.counterTarget.classList.remove(ERROR_CLASS)
+ }
+
+ this.counterTarget.innerHTML = value
+ }
+}
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
index f55774164..8ac0408da 100644
--- a/app/javascript/controllers/index.js
+++ b/app/javascript/controllers/index.js
@@ -26,3 +26,6 @@ application.register("tabs", Tabs)
import { Popover } from "tailwindcss-stimulus-components"
application.register("popover", Popover)
+
+import TextareaAutogrow from "stimulus-textarea-autogrow"
+application.register("textarea-autogrow", TextareaAutogrow)
diff --git a/app/models/release_metadata.rb b/app/models/release_metadata.rb
index 896677e88..b125b08a3 100644
--- a/app/models/release_metadata.rb
+++ b/app/models/release_metadata.rb
@@ -22,21 +22,29 @@ class ReleaseMetadata < ApplicationRecord
IOS_NOTES_MAX_LENGTH = 4000
ANDROID_NOTES_MAX_LENGTH = 500
+ PROMO_TEXT_MAX_LENGTH = 170
+ IOS_DENY_LIST = %w[<]
# NOTE: Refer to https://www.regular-expressions.info/unicode.html for supporting more unicode characters
- PLAINTEXT_REGEX = /\A[~₹!@#$%^&*()_+\-=\[\]{};':"\\|`,.\/?\s\p{Alnum}\p{P}\p{Zs}\p{Emoji_Presentation}\p{M}\p{N}]+\z/
- DEFAULT_LOCALES = ["en-US", "en-GB", "hi-IN", "en-IN", "id"]
- DEFAULT_LOCALE = DEFAULT_LOCALES.first
+ IOS_PLAINTEXT_REGEX = /\A(?!.*#{Regexp.union(IOS_DENY_LIST)})[\p{L}\p{N}\p{P}\p{Sm}\p{Sc}\p{Zs}\p{M}\n]+\z/
+ ANDROID_PLAINTEXT_REGEX = /\A[\p{L}\p{N}\p{P}\p{Sm}\p{Sc}\p{Zs}\p{M}\p{Emoji_Presentation}\p{Extended_Pictographic}\n]+\z/
+ DEFAULT_LOCALE = "en-US"
DEFAULT_LANGUAGE = "English (United States)"
DEFAULT_RELEASE_NOTES = "The latest version contains bug fixes and performance improvements."
validates :release_notes,
- format: {with: PLAINTEXT_REGEX, message: :no_special_characters, multiline: true}
+ format: {with: IOS_PLAINTEXT_REGEX, message: :no_special_characters_ios, denied_characters: IOS_DENY_LIST.join(", "), multiline: true},
+ if: :ios?
+ validates :release_notes,
+ format: {with: ANDROID_PLAINTEXT_REGEX, message: :no_special_characters_android, multiline: true},
+ if: :android?
validates :promo_text,
- format: {with: PLAINTEXT_REGEX, message: :no_special_characters, allow_blank: true, multiline: true},
- length: {maximum: 170}
+ format: {with: IOS_PLAINTEXT_REGEX, message: :no_special_characters, allow_blank: true, multiline: true},
+ length: {maximum: PROMO_TEXT_MAX_LENGTH}
validates :locale, uniqueness: {scope: :release_platform_run_id}
validate :notes_length
+ delegate :ios?, :android?, to: :release_platform_run
+
# NOTE: strip and normalize line endings across various OSes
normalizes :release_notes, with: ->(notes) { notes.strip.gsub("\r\n", "\n") }
@@ -52,10 +60,8 @@ def notes_length
end
def notes_max_length
- case release_platform_run.platform
- when "android" then ANDROID_NOTES_MAX_LENGTH
- when "ios" then IOS_NOTES_MAX_LENGTH
- else raise ArgumentError, "Invalid platform"
- end
+ return ANDROID_NOTES_MAX_LENGTH if android?
+ return IOS_NOTES_MAX_LENGTH if ios?
+ raise ArgumentError, "Invalid platform"
end
end
diff --git a/app/views/shared/_size_limited_textarea.html.erb b/app/views/shared/_size_limited_textarea.html.erb
new file mode 100644
index 000000000..4e59dce3b
--- /dev/null
+++ b/app/views/shared/_size_limited_textarea.html.erb
@@ -0,0 +1,10 @@
+
+ <%= form.labeled_textarea obj_method, label_text, data: { character_counter_target: "input",
+ action: "input->character-counter#update",
+ controller: "textarea-autogrow" } %>
+
+ Characters –
+ <%= existing_value&.length || 0 %>
+ / <%= max_length %>
+
+
diff --git a/config/importmap.rb b/config/importmap.rb
index 17b8e89d5..9b7f70541 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -44,3 +44,4 @@
pin "@sentry-internal/replay", to: "https://ga.jspm.io/npm:@sentry-internal/replay@8.33.1/build/npm/esm/index.js"
pin "@sentry-internal/replay-canvas", to: "https://ga.jspm.io/npm:@sentry-internal/replay-canvas@8.33.1/build/npm/esm/index.js"
pin "tom-select", to: "https://ga.jspm.io/npm:tom-select@2.3.1/dist/js/tom-select.complete.js"
+pin "stimulus-textarea-autogrow", to: "https://ga.jspm.io/npm:stimulus-textarea-autogrow@4.1.0/dist/stimulus-textarea-autogrow.mjs"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 238bdd3cb..cb6f6dcc9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -477,10 +477,11 @@ en:
release_metadata:
attributes:
release_notes:
- no_special_characters: "only allows letters, numbers, emojis, and some special characters"
+ no_special_characters_ios: "for iOS can only contain letters, numbers, punctuation, basic math symbols, currency symbols, and line breaks. The following characters are not allowed – %{denied_characters}"
+ no_special_characters_android: "for Android can only contain letters, numbers, punctuation, basic math symbols, currency symbols, emojis, and line breaks"
too_long: "is too long for %{platform} (maximum is %{max_length} characters)"
promo_text:
- no_special_characters: "only allows letters, numbers, emojis, and some special characters"
+ no_special_characters: "can only contain letters, numbers, punctuation, basic math symbols, currency symbols, and line breaks. The following characters are not allowed – %{denied_characters}"
integration:
format: "%{message}"
attributes:
diff --git a/spec/models/release_metadata_spec.rb b/spec/models/release_metadata_spec.rb
index 5dc0ce78e..05f83e5e5 100644
--- a/spec/models/release_metadata_spec.rb
+++ b/spec/models/release_metadata_spec.rb
@@ -3,27 +3,79 @@
require "rails_helper"
RSpec.describe ReleaseMetadata do
+ let(:locale) { "en-GB" }
+
it "has a valid factory" do
expect(build(:release_metadata)).to be_valid
end
- it "allows emoji characters in notes" do
- expect(build(:release_metadata, promo_text: "😀")).to be_valid
- end
+ context "when iOS" do
+ let(:release_platform) { create(:release_platform, platform: :ios) }
+ let(:release_platform_run) { create(:release_platform_run, release_platform:) }
- it "allows some special characters in notes" do
- expect(build(:release_metadata, promo_text: "Money money money!! ₹100 off! $$ bills yo?! (#money)")).to be_valid
- end
+ it "disallow emoji characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "➡️ something\n😀 💃🏽")).not_to be_valid
+ end
- it "allows accented characters in notes" do
- expect(build(:release_metadata, promo_text: "À la mode, les élèves sont bien à l'aise.")).to be_valid
- end
+ it "allows currencies in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "Money money money!! ₹100 off! => $$ bills yo?! (#money)")).to be_valid
+ end
+
+ it "allows accented characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "À la mode, les élèves sont bien à l'aise.")).to be_valid
+ end
+
+ it "allows non-latin characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "दिल ढूँढता है फिर वही फ़ुरसत के रात दिन, बैठे रहे तसव्वुर-ए-जानाँ किये हुए।")).to be_valid
+ end
+
+ it "allows numbers in non-latin languages in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "१२३४५६७८९१०१११२१३, १३ करूँ गिन गिन के")).to be_valid
+ end
+
+ it "allows up to 4000 characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "a" * 4000)).to be_valid
+ end
- it "allows non-latin characters in notes" do
- expect(build(:release_metadata, promo_text: "दिल ढूँढता है फिर वही फ़ुरसत के रात दिन, बैठे रहे तसव्वुर-ए-जानाँ किये हुए।")).to be_valid
+ it "disallows more than 4000 characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "a" * 4001)).not_to be_valid
+ end
+
+ it "disallows '<' in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "
")).not_to be_valid
+ end
end
- it "allows numbers in non-latin languages in notes" do
- expect(build(:release_metadata, promo_text: "१२३४५६७८९१०१११२१३, १३ करूँ गिन गिन के")).to be_valid
+ context "when android" do
+ let(:release_platform) { create(:release_platform, platform: :android) }
+ let(:release_platform_run) { create(:release_platform_run, release_platform:) }
+
+ it "allows emoji characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "➡️ something\n😀 💃🏽")).to be_valid
+ end
+
+ it "allows currencies in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "Money money money!! ₹100 off! => $$ bills yo?! (#money)")).to be_valid
+ end
+
+ it "allows accented characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "À la mode, les élèves sont bien à l'aise.")).to be_valid
+ end
+
+ it "allows non-latin characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "दिल ढूँढता है फिर वही फ़ुरसत के रात दिन, बैठे रहे तसव्वुर-ए-जानाँ किये हुए।")).to be_valid
+ end
+
+ it "allows numbers in non-latin languages in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "१२३४५६७८९१०१११२१३, १३ करूँ गिन गिन के")).to be_valid
+ end
+
+ it "allows up to 500 characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "a" * 500)).to be_valid
+ end
+
+ it "disallows more than 500 characters in notes" do
+ expect(build(:release_metadata, locale:, release_platform_run:, release_notes: "a" * 501)).not_to be_valid
+ end
end
end