Skip to content

Commit

Permalink
Release notes improvements (#724)
Browse files Browse the repository at this point in the history
- platform specific character checks in notes; disallow emojis in iOS
- add UI hints for number of characters used & max allowed
- add textarea auto-resize as the user inputs increases in size
  • Loading branch information
nid90 authored Jan 20, 2025
1 parent 4171f14 commit 127f8b1
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 43 deletions.
46 changes: 29 additions & 17 deletions app/components/live_release/metadata_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@
<div class="flex flex-col gap-y-2 mb-6">
<div class="flex gap-x-4">
<%= 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 %>
</div>
Expand Down Expand Up @@ -81,7 +81,11 @@
<% f.F.fields_for :android, android_metadata do |aF| %>
<div class="flex flex-col gap-y-4">
<%= aF.hidden_field :id %>
<div><%= aF.labeled_textarea :release_notes, "Release Notes" %></div>
<%= 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 } %>
</div>
<% end %>
<% elsif component.cross_platform? %>
Expand All @@ -94,8 +98,16 @@
<% f.F.fields_for :ios, ios_metadata do |iosF| %>
<div class="flex flex-col gap-y-4">
<%= iosF.hidden_field :id %>
<div><%= iosF.labeled_textarea :release_notes, "What's New?" %></div>
<div><%= iosF.labeled_textarea :promo_text, "Promo Text" %></div>
<%= 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 } %>
<div><%= iosF.labeled_textarea :keywords, "Keywords", readonly: true, disabled: true %></div>
<div><%= iosF.labeled_textarea :description, "Description", readonly: true, disabled: true %></div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions app/components/live_release/metadata_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions app/javascript/controllers/character_counter_controller.js
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
28 changes: 17 additions & 11 deletions app/models/release_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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") }

Expand All @@ -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
10 changes: 10 additions & 0 deletions app/views/shared/_size_limited_textarea.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div data-controller="character-counter" data-character-counter-max-length-value="<%= max_length %>">
<%= form.labeled_textarea obj_method, label_text, data: { character_counter_target: "input",
action: "input->character-counter#update",
controller: "textarea-autogrow" } %>
<p class="text-xs text-secondary">
Characters –
<strong data-character-counter-target="counter"><%= existing_value&.length || 0 %></strong>
/ <%= max_length %>
</p>
</div>
1 change: 1 addition & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@
pin "@sentry-internal/replay", to: "https://ga.jspm.io/npm:@sentry-internal/[email protected]/build/npm/esm/index.js"
pin "@sentry-internal/replay-canvas", to: "https://ga.jspm.io/npm:@sentry-internal/[email protected]/build/npm/esm/index.js"
pin "tom-select", to: "https://ga.jspm.io/npm:[email protected]/dist/js/tom-select.complete.js"
pin "stimulus-textarea-autogrow", to: "https://ga.jspm.io/npm:[email protected]/dist/stimulus-textarea-autogrow.mjs"
5 changes: 3 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
78 changes: 65 additions & 13 deletions spec/models/release_metadata_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<a>")).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

0 comments on commit 127f8b1

Please sign in to comment.