Bị từ chối theo guideline 2.3.7 vì để giá trong screenshot
Talkzy bị App Review từ chối theo guideline 2.3.7 vì screenshot có giá. Khung nào bị dính, mình sửa bộ ảnh ra sao và script nào giờ đang canh phần listing.

Trong bài này
Talkzy là app máy nhắc chữ của mình cho iPhone và iPad: kịch bản chạy ngay dưới camera, có thể chạy theo giọng nói, và mỗi lần quay được lưu thành một clip riêng. Bộ screenshot trên App Store của nó kết thúc bằng khung paywall, tiêu đề "Fair price. No weekly fees." Mình nghĩ đó là một khung minh bạch. App Review thì không nghĩ vậy: bản nộp bị trả về theo guideline 2.3.7, vì có thông tin giá trong screenshot.
Bài này kể ngắn gọn khung nào bị dính, mình đã sửa gì, và giờ mình kiểm tra những gì trước khi đưa bất cứ thứ gì lên App Store Connect. Bản thân phần sửa rất nhỏ. Cái đáng nói là chữ "free" đã len vào bao nhiêu chỗ, và chỗ nào thì script bắt được.
Guideline 2.3.7 thực ra nói gì
2.3.7 là quy định về metadata chính xác: tên app, keyword và phần còn lại của listing. Trong những thứ Apple dặn đừng nhồi vào metadata có thông tin giá. Mà screenshot cũng là metadata. Nên một con số giá trên screenshot, hay một chữ thực chất là lời quảng cáo về giá, đều có thể bị từ chối, kể cả khi giá đúng và khung hình là ảnh chụp thật từ app.
Ý cuối là chỗ mình hớ. Khung paywall không phải mock-up. Nó là ảnh chụp paywall thật trên simulator, đóng khung giống mọi screenshot khác. Chỉ là trong đó có ba mức giá của các gói và một nút mua ghi kèm giá.
Bộ screenshot lúc đó có gì
Bộ iPhone của Talkzy có chín khung, mỗi khung là tiêu đề hai dòng và một dòng phụ phía trên ảnh chụp đặt trong viền máy. Tất cả được ghép bằng một script Python nhỏ (tools/screenshots/compose.py), render chữ bằng Chrome headless. Bộ iPad bảy khung, làm cùng cách.
Ba khung iPhone có vấn đề:
| Khung | Nội dung | Vấn đề |
|---|---|---|
| 05 Mirror mode | "Flip left–right or upside down for beam-splitter glass. Free." | "Free" là một tuyên bố về giá |
| 07 Remotes | "Bluetooth page-turners and keyboards work in every mode. Free." | như trên |
| 09 Paywall | "Fair price. No weekly fees." trên paywall, kèm giá các gói | có giá, cộng thêm "Free with no watermark" ở dòng phụ |
Bộ iPad cũng y như vậy: mirror và remote kết thúc bằng "Free.", và khung paywall ở vị trí 07.
Trong đầu mình, chữ "Free." cuối dòng mirror và remote là mô tả tính năng: mấy cái này dùng không cần trả tiền. Nhìn từ phía reviewer thì đó là giá, và giá là bằng không.

Cách sửa: một commit, bốn dòng caption
Vì các khung được sinh ra từ một danh sách trong script, sửa lỗi chỉ là sửa danh sách đó rồi render lại, không phải ngồi thiết kế lại. Diff phía iPhone trông thế này:
- ('05', 'mirror', 'Mirror mode', 'for prompter rigs',
- 'Flip left–right or upside down for beam-splitter glass. Free.', ''),
+ ('05', 'mirror', 'Mirror mode', 'for prompter rigs',
+ 'Flip left–right or upside down for beam-splitter glass.', ''),
...
- ('09', 'paywall', 'Fair price.', 'No weekly fees.',
- 'Free with no watermark. Pro yearly, monthly or once for life.', ''),Dòng phụ của remote cũng bỏ chữ "Free." theo cách đó, và danh sách iPad được sửa y hệt ba chỗ. Kết quả: bộ iPhone từ chín khung còn tám, bộ iPad từ bảy còn sáu. File khung paywall bị xoá khỏi repo luôn, chứ không chỉ bỏ ra khi upload, để không ai (kể cả mình ba tháng sau) lỡ tay upload lại.
Có hai thứ mình cố ý giữ nguyên:
- Paywall vẫn có screenshot, chỉ là không nằm trong listing. Mỗi in-app purchase trên App Store Connect cần một review screenshot cho chỗ nó được bán. Talkzy giữ riêng một ảnh paywall sạch cho đúng ô đó. Ảnh này chỉ App Review thấy, và đó mới là chỗ của giá.
- Giá vẫn nằm trong description. Subscription tự gia hạn bắt buộc phải nêu điều khoản, nên description vẫn ghi các gói, free trial của gói năm và cách gia hạn. Quy định nhắm vào screenshot và các trường được index, không phải bắt mình giấu giá.
Đây là ba khung đã lên sau khi sửa. Không giá, không "free", chỉ có app làm được gì:

Sửa luôn cho app khác trước khi bị từ chối
Script ghép screenshot của Talkzy vốn được chuyển từ một app khác của mình sang, và Baton cũng làm theo cùng cách. Ngay sáng hôm đó mình đi tìm đúng pattern này ở chỗ khác. Baton, app theo dõi em bé của mình (baton.io.vn), có khung cuối với tiêu đề "Free forever. Or pay once." và giá gói Plus ở dòng phụ, cộng thêm một khung biểu đồ trên iPad mà dòng phụ nói phần nào miễn phí. Cùng commit message, cùng cách sửa: bỏ khung paywall, cắt dòng phụ iPad, bộ iPhone từ mười khung còn chín.
Bài học này rẻ nên áp dụng hai lần càng tốt. Một lần bị từ chối tốn nguyên một vòng review. Sửa trước cho app anh em khi nó chưa bị review thì chỉ tốn vài phút.
Biến bài học thành một bước kiểm tra
Một lần bị từ chối mà chỉ nằm trong trí nhớ thì sớm muộn sẽ lặp lại. Nên cùng ngày, script kiểm tra metadata vốn đã validate listing 10 ngôn ngữ của Talkzy được thêm phần 2.3.7. Nó đọc docs/app-store/locales/*.json và báo lỗi khi các trường được index có thứ gì trông giống giá:
# 2.3.7: no price / free / superlative in the indexed fields; no competitor or platform brands.
BANNED = ['free', 'gratis', 'gratuit', 'kostenlos', 'miễn phí', '無料', '免费', '무료',
'best', 'tốt nhất', ...]
PRICE = re.compile(r"[$€£¥₫]|\d+\s*(k|đ|usd|vnd)\b|\d", re.I)Script kiểm name, subtitle và keywords gộp lại, vì Apple index ba trường đó cùng nhau, và thoát với mã 1 nếu có lỗi. Quy tắc của mình đơn giản: chạy nó trước khi dán bất cứ thứ gì vào App Store Connect. Đợt ASO ngay sau đó (chi tiết ở bài ASO cho 10 ngôn ngữ không cần tool trả phí) cũng bỏ "free", "no weekly plans" và "no watermark" khỏi promotional text. Trường promo không nằm trong phạm vi chữ của 2.3.7, nhưng đó là thứ reviewer đọc đầu tiên, và nó cũng không nên dựa vào chuyện giá.

Nói thật về lỗ hổng còn lại: script canh các trường chữ, không canh ảnh. Caption của screenshot nằm trong script ghép, còn chữ trong UI của app thì nằm ngay trong ảnh chụp. Mấy chỗ đó mình vẫn duyệt bằng mắt, từng khung một, trước mỗi lần upload. Một chữ "free" trong màn hình cài đặt hay một bộ đếm lượt dùng được chụp lại thì reviewer vẫn thấy rõ như ở tiêu đề.
Rút ra
- Coi screenshot là metadata, vì nó đúng là metadata. Ảnh chụp thật của một paywall thật vẫn có thể vi phạm 2.3.7.
- "Free" cũng là giá. Caption nào kết thúc bằng "Free." thì viết lại để nói tính năng làm được gì.
- Để paywall đúng chỗ: review screenshot của in-app purchase, còn điều khoản subscription thì ở description.
- Sinh khung hình từ một danh sách. Bị từ chối thì chỉ là diff hai dòng và render lại, không phải thiết kế lại.
- Biến mỗi lần bị từ chối thành một bước kiểm tra, và nói rõ bước đó không bao được những gì.
Thông tin thêm về Talkzy ở talkzy.io.vn, các app còn lại của mình ở apps.vanthuongdao.id.vn. Nếu bạn sắp ship in-app purchase đầu tiên, bài tiếp theo nói về một cái bẫy khác của App Store Connect: chuyện gì xảy ra khi rút một bản nộp đang mang IAP đầu tiên.
Bài liên quan


