diff --git a/apps/extension/CHROMEWEBSTORE.md b/apps/extension/CHROMEWEBSTORE.md index 5f79ca19b..56bc201b9 100644 --- a/apps/extension/CHROMEWEBSTORE.md +++ b/apps/extension/CHROMEWEBSTORE.md @@ -45,7 +45,7 @@ This table lists exactly what `manifest.json` requests; `test-extension-pipeline 1. **Where it runs:** the content script loads on the 10 supported stores listed above (`shopee.vn`, `lazada.vn`, `tiki.vn`, TikTok Shop product pages on `www.tiktok.com` and `shop.tiktok.com`, `cellphones.com.vn`, `thegioididong.com`, `fptshop.com.vn`, `hoanghamobile.com`, `didongviet.vn`, `phongvu.vn`). It acts only on pages it recognises as a product page. The extension itself never opens or fetches pages the user did not open, never works around a store's bot checks and never solves captchas. 2. **Lookup (on by default, no opt-in):** on a recognised product page the extension sends `GET /api/v1/rate/extension/lookup` with the store, the product URL with query parameters and tracking tokens (`utm_*`, `sp_atk`, `xptdk`, `fbclid`, `gclid`) stripped locally, the product title, the store's item id and shop id, the selected variant, the listed price and the original price. The request is read-only and carries no account, name, email or cookies. -3. **Opt-in crowd signal (off by default):** only when the user turns on "Đóng góp phát hiện sản phẩm mới (chỉ khi bạn bật)" (English: "Help find new products (only when you turn it on)") in the Side Panel, the extension also sends `POST /api/v1/rate/extension/ingest` with the store, item id, shop id, cleaned URL, title, selected variant, price, original price, rating and rating count. The toggle's label lists these same fields (DOS/Rate#1230). Submissions go to a moderation queue, stored with the time the server received them; Rate.vn's servers may re-check the listing on the store. They never change public prices directly. +3. **Opt-in crowd signal (off by default):** only when the user chooses "Có" / "Yes" on the consent page opened once at first install, or turns on "Đóng góp phát hiện sản phẩm mới (chỉ khi bạn bật)" (English: "Help find new products (only when you turn it on)") in the Side Panel, the extension also sends `POST /api/v1/rate/extension/ingest` with the store, item id, shop id, cleaned URL, title, selected variant, price, original price, rating and rating count. The toggle's label lists these same fields (DOS/Rate#1230). Submissions go to a moderation queue, stored with the time the server received them; Rate.vn's servers may re-check the listing on the store. They never change public prices directly. Closing the consent page keeps the opt-in off, and updates or reloads do not open it again (DOS/Rate#1603). 4. **Stored in the browser only:** the server address and the opt-in choice (`chrome.storage.local`), and a session-only cache of the products viewed in each tab, the latest side panel search, their lookup results and a lookup wait time after a rate limit (`chrome.storage.session`). 5. **IP address:** like any web request, the Rate.vn server receives the user's IP address. The lookup and opt-in routes use it only for a per-minute request limit held in memory, and Rate.vn's code does not store it; the hosting provider keeps standard access logs for a short time. 6. **Side panel requests:** text the user types into the side panel search is sent to the same lookup endpoint; the side panel also reads public deals and merchant lists from the Rate.vn API. diff --git a/apps/extension/RELEASE.md b/apps/extension/RELEASE.md index 3d41739bd..2c44ff297 100644 --- a/apps/extension/RELEASE.md +++ b/apps/extension/RELEASE.md @@ -26,6 +26,7 @@ Release-readiness check of the extension built from `dev` at `fc9b820d` on 2026- - **Product pages:** on one product page each of Shopee, Tiki and CellphoneS, the pill appears and the side panel opens. - **Language:** set Chrome to English, then Czech. The panel and the verdict follow the browser language, and en/cs never show the Vietnamese review summary (#1385). - **Opt-in:** the crowd opt-in is off by default, and its label names the fields it sends (#1230, #1241). + - **Consent page:** a fresh install opens the consent page once, with "No" focused. Closing it leaves the opt-in off; "Yes" turns on the side panel toggle; reloading or updating the extension does not open it again (#1603). 5. Update the store listing: - Description and screenshots match the panel as it is now (the price list and the states from #1319). - The changelog below. @@ -44,7 +45,7 @@ Release-readiness check of the extension built from `dev` at `fc9b820d` on 2026- - Biểu tượng trên thanh công cụ cho biết nhanh giá đang xem có tốt không. Tin bán đã hết hàng không còn được tính vào giá so sánh. - Nhận diện sản phẩm chính xác hơn: tra "iPhone 16" không còn ra iPhone 16e. Trang danh mục và trang thương hiệu không bị nhầm là trang sản phẩm. - Gian hàng chưa có đánh giá được phân tích sẽ ghi "Chưa có dữ liệu", không hiện con số rủi ro. -- Lựa chọn "Đóng góp phát hiện sản phẩm mới" ghi rõ những gì được gửi đi và vẫn tắt mặc định. TikTok chỉ còn chạy trên trang sản phẩm. +- Lựa chọn "Đóng góp phát hiện sản phẩm mới" ghi rõ những gì được gửi đi và vẫn tắt mặc định. Lần cài đầu tiên, tiện ích hỏi bạn một lần. TikTok chỉ còn chạy trên trang sản phẩm. ### English @@ -55,4 +56,4 @@ Release-readiness check of the extension built from `dev` at `fc9b820d` on 2026- - The toolbar badge shows at a glance whether the price you see is a good one. Sold-out listings no longer count towards the price comparison. - More accurate product matching: "iPhone 16" no longer matches the iPhone 16e. Category and brand pages are no longer mistaken for product pages. - Sellers with no analysed reviews show "No data yet" instead of a risk number. -- The "Help find new products" option names exactly what it sends and is still off by default. On TikTok, the extension runs only on product pages. +- The "Help find new products" option names exactly what it sends and is still off by default. On first install, the extension asks you once. On TikTok, the extension runs only on product pages. diff --git a/apps/extension/scripts/build.mjs b/apps/extension/scripts/build.mjs index f8b0892fd..7486dd105 100644 --- a/apps/extension/scripts/build.mjs +++ b/apps/extension/scripts/build.mjs @@ -16,6 +16,7 @@ async function main() { fs.rmSync(distDir, { recursive: true, force: true }); ensureDir(path.join(distDir, 'icons')); ensureDir(path.join(distDir, 'sidepanel')); + ensureDir(path.join(distDir, 'onboarding')); // 1. Copy manifest.json fs.copyFileSync( @@ -53,8 +54,13 @@ async function main() { ...common, format: 'esm', entryPoints: { - 'background/service-worker': path.join(srcDir, 'background', 'service-worker.ts'), + 'background/service-worker': path.join( + srcDir, + 'background', + 'service-worker.ts' + ), 'sidepanel/sidepanel': path.join(srcDir, 'sidepanel', 'sidepanel.ts'), + 'onboarding/onboarding': path.join(srcDir, 'onboarding', 'onboarding.ts'), }, outdir: distDir, }); @@ -62,7 +68,11 @@ async function main() { ...common, format: 'iife', entryPoints: { - 'content/content-script': path.join(srcDir, 'content', 'content-script.ts'), + 'content/content-script': path.join( + srcDir, + 'content', + 'content-script.ts' + ), }, outdir: distDir, }); @@ -70,6 +80,11 @@ async function main() { path.join(srcDir, 'sidepanel', 'sidepanel.html'), path.join(distDir, 'sidepanel', 'sidepanel.html') ); + // The install consent page (DOS/Rate#1603). + fs.copyFileSync( + path.join(srcDir, 'onboarding', 'onboarding.html'), + path.join(distDir, 'onboarding', 'onboarding.html') + ); console.log( '✅ Built Rate Browser Extension (Manifest V3) into apps/extension/dist' diff --git a/apps/extension/src/_locales/cs/messages.json b/apps/extension/src/_locales/cs/messages.json index 6d2564ce0..f61b6bbf7 100644 --- a/apps/extension/src/_locales/cs/messages.json +++ b/apps/extension/src/_locales/cs/messages.json @@ -652,5 +652,38 @@ "content": "$2" } } + }, + "onboardingTitle": { + "message": "Rate.vn je připraveno" + }, + "onboardingLookup": { + "message": "Na stránkách produktů 10 podporovaných obchodů rozšíření posílá Rate.vn obchod, URL bez parametrů, název, ID produktu a obchodu, variantu, cenu a původní cenu, aby vyhledalo historii cen. Účet není potřeba." + }, + "onboardingQuestion": { + "message": "Chcete přispívat nabídkami, které si prohlížíte?" + }, + "onboardingNothingYet": { + "message": "Ve výchozím stavu vypnuto. Změnit to můžete kdykoli: klikněte na ikonu Rate.vn, otevře se boční panel, volba „Pomoc s hledáním nových produktů“." + }, + "onboardingNo": { + "message": "Ne, děkuji" + }, + "onboardingYes": { + "message": "Ano, chci přispívat" + }, + "onboardingSavedNo": { + "message": "Uloženo: nepřispívat. Tuto stránku můžete zavřít." + }, + "onboardingSavedYes": { + "message": "Uloženo: přispívat. Vypnout to můžete v bočním panelu." + }, + "onboardingSaveFailed": { + "message": "Neuloženo. Zkuste to znovu nebo zvolte v bočním panelu." + }, + "onboardingPrivacyLink": { + "message": "Zásady ochrany soukromí" + }, + "onboardingOptInBody": { + "message": "Když zvolíte „Ano“, každá nabídka, kterou otevřete, se také odešle do fronty ke kontrole Rate.vn s těmito údaji: obchod, ID produktu, ID obchodu, URL bez sledovacích parametrů, název produktu, zvolená varianta, cena, původní cena, počet hvězdiček a počet recenzí. Servery Rate.vn mohou nabídku v obchodě znovu ověřit. Bez účtu, jména, e-mailu a cookies." } } diff --git a/apps/extension/src/_locales/en/messages.json b/apps/extension/src/_locales/en/messages.json index f69af4fcd..2843358d2 100644 --- a/apps/extension/src/_locales/en/messages.json +++ b/apps/extension/src/_locales/en/messages.json @@ -652,5 +652,38 @@ "content": "$2" } } + }, + "onboardingTitle": { + "message": "Rate.vn is ready" + }, + "onboardingLookup": { + "message": "On product pages of the 10 supported stores, the extension sends Rate.vn the store, the URL without parameters, the title, the item and shop ids, the variant, the price and the original price to look up price history. No account needed." + }, + "onboardingQuestion": { + "message": "Would you like to contribute the listings you view?" + }, + "onboardingNothingYet": { + "message": "Off by default. Change it any time: click the Rate.vn icon to open the side panel, \"Help find new products\"." + }, + "onboardingNo": { + "message": "No, thanks" + }, + "onboardingYes": { + "message": "Yes, I want to contribute" + }, + "onboardingSavedNo": { + "message": "Saved: not contributing. You can close this page." + }, + "onboardingSavedYes": { + "message": "Saved: contributing. You can turn it off in the side panel." + }, + "onboardingSaveFailed": { + "message": "Not saved. Try again or choose in the side panel." + }, + "onboardingPrivacyLink": { + "message": "Privacy policy" + }, + "onboardingOptInBody": { + "message": "If you choose \"Yes\", every listing you open is also sent to the Rate.vn review queue with: store, product ID, shop ID, URL with tracking parameters removed, product name, selected variant, price, original price, star rating and number of reviews. Rate.vn's servers may re-check that listing on the store. No account, name, email or cookies are included." } } diff --git a/apps/extension/src/_locales/vi/messages.json b/apps/extension/src/_locales/vi/messages.json index 188394535..355d9682a 100644 --- a/apps/extension/src/_locales/vi/messages.json +++ b/apps/extension/src/_locales/vi/messages.json @@ -652,5 +652,38 @@ "content": "$2" } } + }, + "onboardingTitle": { + "message": "Rate.vn đã sẵn sàng" + }, + "onboardingLookup": { + "message": "Trên trang sản phẩm của 10 sàn được hỗ trợ, tiện ích gửi cho Rate.vn sàn, URL đã bỏ tham số, tên, mã sản phẩm và mã gian hàng, phiên bản, giá bán và giá gốc để tra lịch sử giá. Không cần tài khoản." + }, + "onboardingQuestion": { + "message": "Bạn có muốn đóng góp các tin bán bạn xem không?" + }, + "onboardingNothingYet": { + "message": "Mặc định tắt. Bạn đổi được bất cứ lúc nào: bấm biểu tượng Rate.vn để mở bảng bên cạnh, mục \"Đóng góp phát hiện sản phẩm mới\"." + }, + "onboardingNo": { + "message": "Không, cảm ơn" + }, + "onboardingYes": { + "message": "Có, tôi muốn đóng góp" + }, + "onboardingSavedNo": { + "message": "Đã lưu: không đóng góp. Bạn có thể đóng trang này." + }, + "onboardingSavedYes": { + "message": "Đã lưu: bật đóng góp. Bạn tắt được trong bảng bên cạnh." + }, + "onboardingSaveFailed": { + "message": "Chưa lưu được. Hãy thử lại hoặc chọn trong bảng bên cạnh." + }, + "onboardingPrivacyLink": { + "message": "Chính sách bảo mật" + }, + "onboardingOptInBody": { + "message": "Nếu bạn chọn \"Có\", mỗi tin bán bạn mở còn được gửi vào hàng đợi kiểm duyệt của Rate.vn với: sàn, mã sản phẩm, mã gian hàng, URL đã cắt tham số theo dõi, tên sản phẩm, phiên bản đang chọn, giá bán, giá gốc, số sao và số lượt đánh giá. Máy chủ Rate.vn có thể kiểm tra lại tin bán đó trên sàn. Không kèm tài khoản, tên, email hay cookie." } } diff --git a/apps/extension/src/background/service-worker.ts b/apps/extension/src/background/service-worker.ts index 0ba4c584a..268352c08 100644 --- a/apps/extension/src/background/service-worker.ts +++ b/apps/extension/src/background/service-worker.ts @@ -12,8 +12,10 @@ import { import { buildCrowdIngestPayload, DEFAULT_API_BASE_URL, + isOnboardingSender, isPrivilegedExtensionSender, normalizeAllowedApiBaseUrl, + ONBOARDING_PAGE_PATH, sanitizeExtensionProductSignal, selectFreshestSessionSignal, } from '../security-guards.js'; @@ -209,22 +211,39 @@ async function updateActionBadge( } } -chrome.runtime.onInstalled.addListener(async () => { +chrome.runtime.onInstalled.addListener(async (details) => { try { await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); + // Each missing setting gets its default; a stored choice is kept. const existing = await chrome.storage.local.get([ 'apiBaseUrl', 'crowdContributeOptIn', ]); + const defaults: Partial = {}; if (typeof existing.apiBaseUrl !== 'string') { - await chrome.storage.local.set({ - apiBaseUrl: DEFAULT_SETTINGS.apiBaseUrl, - crowdContributeOptIn: DEFAULT_SETTINGS.crowdContributeOptIn, - }); + defaults.apiBaseUrl = DEFAULT_SETTINGS.apiBaseUrl; + } + if (typeof existing.crowdContributeOptIn !== 'boolean') { + defaults.crowdContributeOptIn = DEFAULT_SETTINGS.crowdContributeOptIn; + } + if (Object.keys(defaults).length > 0) { + await chrome.storage.local.set(defaults); } } catch { // Ignore sidePanel or storage init errors during extension reload } + // On a first install only, after the defaults, ask about the opt-in + // (DOS/Rate#1603). The default stays off, so closing the page sends + // nothing extra. + if (details?.reason === 'install') { + try { + await chrome.tabs.create({ + url: chrome.runtime.getURL(ONBOARDING_PAGE_PATH), + }); + } catch { + // The side panel toggle still offers the choice. + } + } }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { @@ -566,7 +585,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } if (msgType === 'RATE_UPDATE_SETTINGS') { - if (!isPrivilegedExtensionSender(sender)) { + // The install consent page may set the opt-in and nothing else. + const fromOnboarding = isOnboardingSender(sender); + if (!fromOnboarding && !isPrivilegedExtensionSender(sender)) { sendResponse({ ok: false, error: 'Settings can only be updated from the Rate extension sidepanel', @@ -577,6 +598,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { try { const nextSettings: Partial = {}; if ( + !fromOnboarding && typeof message.apiBaseUrl === 'string' && message.apiBaseUrl.trim() ) { diff --git a/apps/extension/src/onboarding/onboarding.html b/apps/extension/src/onboarding/onboarding.html new file mode 100644 index 000000000..0f393d878 --- /dev/null +++ b/apps/extension/src/onboarding/onboarding.html @@ -0,0 +1,138 @@ + + + + + + + + Rate.vn + + + +
+
+

+

+

+

+

+
+ + +
+

+

+ +

+
+
+ + + diff --git a/apps/extension/src/onboarding/onboarding.ts b/apps/extension/src/onboarding/onboarding.ts new file mode 100644 index 000000000..bf8ba484f --- /dev/null +++ b/apps/extension/src/onboarding/onboarding.ts @@ -0,0 +1,73 @@ +/** + * The consent screen shown once at install (DOS/Rate#1603, JOY 2026-11-04: + * ask at install and keep the side panel toggle, default off). The choice + * goes to the same setting as the side panel toggle; closing the page leaves + * the default, off, so nothing extra is sent. + */ +import { applyDocumentMessages, t } from '../i18n.js'; + +let busy = false; + +/** Marks the stored choice on its button. */ +function markChoice(optIn: boolean | null): void { + for (const [id, value] of [ + ['btn-no', false], + ['btn-yes', true], + ] as const) { + document + .getElementById(id) + ?.setAttribute('aria-pressed', String(optIn === value)); + } +} + +async function choose(optIn: boolean): Promise { + // A flag, not disabled buttons, so keyboard focus stays where it was. + if (busy) return; + busy = true; + const status = document.getElementById('status'); + // Cleared first, so the same choice made twice is announced again. + if (status) status.textContent = ''; + try { + const res = (await chrome.runtime.sendMessage({ + type: 'RATE_UPDATE_SETTINGS', + crowdContributeOptIn: optIn, + })) as { ok?: boolean; settings?: { crowdContributeOptIn?: boolean } }; + if (!res?.ok || res.settings?.crowdContributeOptIn !== optIn) { + throw new Error('not saved'); + } + markChoice(optIn); + if (status) { + status.textContent = t( + optIn ? 'onboardingSavedYes' : 'onboardingSavedNo' + ); + } + } catch { + if (status) status.textContent = t('onboardingSaveFailed'); + } finally { + busy = false; + } +} + +document.addEventListener('DOMContentLoaded', () => { + applyDocumentMessages(document); + document.title = t('onboardingTitle'); + document + .getElementById('btn-no') + ?.addEventListener('click', () => void choose(false)); + document + .getElementById('btn-yes') + ?.addEventListener('click', () => void choose(true)); + // The status always shows the stored choice, also one changed in the side + // panel after this page saved it. + if (typeof chrome === 'undefined') return; + chrome.storage.onChanged?.addListener((changes, areaName) => { + const change = changes.crowdContributeOptIn; + const status = document.getElementById('status'); + if (areaName === 'local' && change && status) { + markChoice(change.newValue === true); + status.textContent = t( + change.newValue === true ? 'onboardingSavedYes' : 'onboardingSavedNo' + ); + } + }); +}); diff --git a/apps/extension/src/security-guards.ts b/apps/extension/src/security-guards.ts index 869032254..d25026002 100644 --- a/apps/extension/src/security-guards.ts +++ b/apps/extension/src/security-guards.ts @@ -65,6 +65,31 @@ export function normalizeAllowedApiBaseUrl( } } +/** The consent page opened once at install (DOS/Rate#1603). */ +export const ONBOARDING_PAGE_PATH = 'onboarding/onboarding.html'; + +/** + * The install consent page, an extension page in a tab: exactly its URL in + * this extension, never a content script (whose url is the web page). It may + * only set the opt-in (DOS/Rate#1603). + */ +export function isOnboardingSender( + sender: { id?: string; tab?: unknown; url?: string } | null | undefined, + expectedRuntimeId?: string +): boolean { + if (!sender || typeof sender !== 'object' || Array.isArray(sender)) { + return false; + } + const runtimeId = + expectedRuntimeId ?? + (typeof chrome !== 'undefined' ? chrome.runtime?.id : undefined); + if (!runtimeId) return false; + if (typeof sender.id === 'string' && sender.id !== runtimeId) return false; + return ( + sender.url === `chrome-extension://${runtimeId}/${ONBOARDING_PAGE_PATH}` + ); +} + export function isPrivilegedExtensionSender( sender: { id?: string; @@ -76,9 +101,6 @@ export function isPrivilegedExtensionSender( if (!sender || typeof sender !== 'object' || Array.isArray(sender)) { return false; } - if (sender.tab != null) { - return false; - } const runtimeId = expectedRuntimeId ?? (typeof globalThis !== 'undefined' && @@ -90,6 +112,10 @@ export function isPrivilegedExtensionSender( .runtime!.id : undefined); + if (sender.tab != null) { + return false; + } + if ( runtimeId && typeof sender.id === 'string' && diff --git a/apps/extension/src/sidepanel/sidepanel.ts b/apps/extension/src/sidepanel/sidepanel.ts index 314aa2424..bb016e453 100644 --- a/apps/extension/src/sidepanel/sidepanel.ts +++ b/apps/extension/src/sidepanel/sidepanel.ts @@ -820,6 +820,13 @@ document.addEventListener('DOMContentLoaded', () => { } }); + chrome.storage.onChanged?.addListener((changes, areaName) => { + const change = changes.crowdContributeOptIn; + if (areaName === 'local' && change && chkOptIn) { + chkOptIn.checked = change.newValue === true; + } + }); + chkOptIn?.addEventListener('change', async () => { await chrome.runtime.sendMessage({ type: 'RATE_UPDATE_SETTINGS', diff --git a/apps/extension/src/test-extension-pipeline.ts b/apps/extension/src/test-extension-pipeline.ts index c641cfe40..a7ffce1d3 100644 --- a/apps/extension/src/test-extension-pipeline.ts +++ b/apps/extension/src/test-extension-pipeline.ts @@ -19,6 +19,7 @@ import { DEFAULT_API_BASE_URL, MAX_SPARKLINE_POINTS, escapeSafeExternalHref, + isOnboardingSender, isPrivilegedExtensionSender, normalizeAllowedApiBaseUrl, normalizeFiveStarRating, @@ -467,6 +468,52 @@ async function runTests() { }), true ); + // The install consent page is an extension page in a tab: it is never a + // privileged sender, and only isOnboardingSender, which lets it set the + // opt-in alone, accepts it by its exact URL (DOS/Rate#1603). + const onboardingUrl = + 'chrome-extension://abcdefghijklmnop/onboarding/onboarding.html'; + assert.equal( + isPrivilegedExtensionSender( + { tab: { id: 7 }, url: onboardingUrl }, + 'abcdefghijklmnop' + ), + false + ); + assert.equal( + isOnboardingSender( + { tab: { id: 7 }, url: onboardingUrl }, + 'abcdefghijklmnop' + ), + true + ); + for (const sender of [ + { tab: { id: 7 }, url: 'https://shopee.vn/onboarding/onboarding.html' }, + { + tab: { id: 7 }, + url: 'chrome-extension://otherextensionid/onboarding/onboarding.html', + }, + { + tab: undefined, + url: 'chrome-extension://abcdefghijklmnop/sidepanel/sidepanel.html', + }, + { tab: { id: 7 }, url: `${onboardingUrl}?x=1` }, + { tab: { id: 7 }, url: `${onboardingUrl}#x` }, + { tab: { id: 7 }, url: undefined }, + { id: 'otherextensionid', tab: { id: 7 }, url: onboardingUrl }, + null, + ]) { + assert.equal( + isOnboardingSender(sender as never, 'abcdefghijklmnop'), + false, + JSON.stringify(sender) + ); + } + assert.equal( + isOnboardingSender({ tab: { id: 7 }, url: onboardingUrl }), + false, + 'without a known runtime id nothing is the consent page' + ); assert.equal( escapeSafeExternalHref('javascript:alert(1)'), 'https://rate.vn', @@ -605,7 +652,23 @@ async function runTests() { ); } assert.ok(!/Chỉ gửi URL/.test(optInText)); + // The install consent page names the same fields (DOS/Rate#1603). + const consentText = catalogue.onboardingOptInBody?.message ?? ''; + for (const key of Object.values(CROWD_INGEST_FIELD_LABELS)) { + assert.ok( + consentText.includes(catalogue[key]?.message ?? '\u0000'), + `${locale} consent text lacks ${key}` + ); + } } + assert.ok( + fs + .readFileSync( + path.resolve(__dirname, 'onboarding/onboarding.html'), + 'utf8' + ) + .includes('data-i18n="onboardingOptInBody"') + ); // The store listing names the extension as the vi catalogue does. const viName = readLocaleCatalogues().vi.extName?.message; assert.equal( diff --git a/apps/web/locales/cs.json b/apps/web/locales/cs.json index bd829dc46..b9c405417 100644 --- a/apps/web/locales/cs.json +++ b/apps/web/locales/cs.json @@ -1534,7 +1534,7 @@ "privacyS4Title": "4. Soukromé údaje prodejců", "privacyS4Desc": "Soukromé údaje prodejce (telefonní číslo, bankovní účet) jsou v polích, která veřejnost nemůže číst. Web i API mohou číst jen veřejná pole.", "privacyS5Title": "5. Rozšíření prohlížeče Rate.vn", - "privacyS5Desc": "Na stránkách produktů 10 podporovaných obchodů rozšíření posílá Rate.vn obchod, URL produktu bez parametrů, název produktu, ID položky a obchodu, zvolenou variantu, cenu a původní cenu, aby vyhledalo historii cen; toto vyhledávání je zapnuté ve výchozím stavu, jen čte a nepotřebuje účet. Text, který napíšete do vyhledávacího pole rozšíření, se také odešle k vyhledání. Jen když zapnete \"Pomoc s hledáním nových produktů (jen když ji zapnete)\" (ve výchozím stavu vypnuto), posílá také danou nabídku s hodnocením a počtem hodnocení do fronty ke kontrole, uloženou s časem, kdy ji server přijal; servery Rate.vn mohou nabídku v obchodě znovu ověřit a tato data sama veřejné ceny nemění. Rozšíření neposílá účet, jméno, e-mail ani cookies, neotevírá stránky, které jste neotevřeli, a ve vašem prohlížeči ukládá jen adresu serveru, volbu opt-in a mezipaměť platnou po dobu relace s produkty, které jste si prohlédli, vaším posledním hledáním, výsledky vyhledávání a dobou čekání na další vyhledání. Jako u každého webového požadavku server obdrží vaši IP adresu: kód Rate.vn ji používá jen v paměti pro limit požadavků za minutu a náš poskytovatel hostingu uchovává běžné přístupové záznamy po krátkou dobu.", + "privacyS5Desc": "Na stránkách produktů 10 podporovaných obchodů rozšíření posílá Rate.vn obchod, URL produktu bez parametrů, název produktu, ID položky a obchodu, zvolenou variantu, cenu a původní cenu, aby vyhledalo historii cen; toto vyhledávání je zapnuté ve výchozím stavu, jen čte a nepotřebuje účet. Text, který napíšete do vyhledávacího pole rozšíření, se také odešle k vyhledání. Jen když zvolíte \"Ano\" na stránce zobrazené při instalaci nebo zapnete \"Pomoc s hledáním nových produktů (jen když ji zapnete)\" v bočním panelu (ve výchozím stavu vypnuto), posílá také danou nabídku s hodnocením a počtem hodnocení do fronty ke kontrole, uloženou s časem, kdy ji server přijal; servery Rate.vn mohou nabídku v obchodě znovu ověřit a tato data sama veřejné ceny nemění. Rozšíření neposílá účet, jméno, e-mail ani cookies, neotevírá stránky, které jste neotevřeli, a ve vašem prohlížeči ukládá jen adresu serveru, volbu opt-in a mezipaměť platnou po dobu relace s produkty, které jste si prohlédli, vaším posledním hledáním, výsledky vyhledávání a dobou čekání na další vyhledání. Jako u každého webového požadavku server obdrží vaši IP adresu: kód Rate.vn ji používá jen v paměti pro limit požadavků za minutu a náš poskytovatel hostingu uchovává běžné přístupové záznamy po krátkou dobu.", "privacySide1Title": "Ověřte si otevřený kód", "privacySide1Desc": "Pravidla přístupu k datům a způsob ukládání hlasů jsou veřejné v repozitáři na GitHubu.", "privacySide1Btn": "Technická a API dokumentace", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index b7d1b41df..fc5960b1f 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -1534,7 +1534,7 @@ "privacyS4Title": "4. Sellers' private details", "privacyS4Desc": "A seller's private details (phone number, bank account) are kept in fields the public cannot read. The website and the API can only read the public fields.", "privacyS5Title": "5. The Rate.vn browser extension", - "privacyS5Desc": "On product pages of the 10 supported stores, the extension sends Rate.vn the store, the product URL without its parameters, the product title, the item and shop ids, the selected variant, the price and the original price to look up price history; this lookup is on by default, read-only and needs no account. Text you type into the extension's search box is also sent to look it up. Only if you turn on \"Help find new products (only when you turn it on)\" (off by default) does it also send that listing, with its rating and rating count, to a moderation queue, stored with the time the server received it; Rate.vn's servers may re-check that listing on the store, and this data does not change public prices by itself. The extension sends no account, name, email or cookies, opens no page you did not open, and keeps only the server address, the opt-in choice and a session-only cache of the products you viewed, your latest search, their lookup results and a lookup wait time in your browser. Like any web request, the server receives your IP address: Rate.vn's code uses it only in memory for a per-minute request limit, and our hosting provider keeps standard access logs for a short time.", + "privacyS5Desc": "On product pages of the 10 supported stores, the extension sends Rate.vn the store, the product URL without its parameters, the product title, the item and shop ids, the selected variant, the price and the original price to look up price history; this lookup is on by default, read-only and needs no account. Text you type into the extension's search box is also sent to look it up. Only if you choose \"Yes\" on the page shown at install or turn on \"Help find new products (only when you turn it on)\" in the side panel (off by default) does it also send that listing, with its rating and rating count, to a moderation queue, stored with the time the server received it; Rate.vn's servers may re-check that listing on the store, and this data does not change public prices by itself. The extension sends no account, name, email or cookies, opens no page you did not open, and keeps only the server address, the opt-in choice and a session-only cache of the products you viewed, your latest search, their lookup results and a lookup wait time in your browser. Like any web request, the server receives your IP address: Rate.vn's code uses it only in memory for a per-minute request limit, and our hosting provider keeps standard access logs for a short time.", "privacySide1Title": "Check the open code", "privacySide1Desc": "The data access rules and how votes are stored are public in the GitHub repository.", "privacySide1Btn": "Technical and API docs", diff --git a/apps/web/locales/vi.json b/apps/web/locales/vi.json index a7009f971..c4564139d 100644 --- a/apps/web/locales/vi.json +++ b/apps/web/locales/vi.json @@ -1534,7 +1534,7 @@ "privacyS4Title": "4. Thông tin riêng của gian hàng", "privacyS4Desc": "Thông tin riêng của gian hàng (số điện thoại, tài khoản ngân hàng) nằm ở các trường mà công chúng không đọc được. Trang web và API chỉ đọc được các trường công khai.", "privacyS5Title": "5. Tiện ích trình duyệt Rate.vn", - "privacyS5Desc": "Trên trang sản phẩm của 10 sàn được hỗ trợ, tiện ích gửi cho Rate.vn sàn, URL sản phẩm đã bỏ tham số, tên sản phẩm, mã sản phẩm và mã gian hàng, phiên bản đang chọn, giá bán và giá gốc để tra lịch sử giá; việc tra cứu này bật sẵn, chỉ đọc và không cần tài khoản. Nội dung bạn gõ vào ô tìm kiếm của tiện ích cũng được gửi để tra cứu. Chỉ khi bạn bật \"Đóng góp phát hiện sản phẩm mới (chỉ khi bạn bật)\" (mặc định tắt), tiện ích còn gửi tin bán đó kèm số sao và số lượt đánh giá vào hàng đợi kiểm duyệt, lưu kèm thời điểm máy chủ nhận; máy chủ Rate.vn có thể kiểm tra lại tin bán đó trên sàn, và dữ liệu này không tự đổi giá công khai. Tiện ích không gửi tài khoản, tên, email hay cookie, không mở trang bạn không mở, và chỉ lưu trên trình duyệt của bạn địa chỉ máy chủ, lựa chọn có đóng góp hay không, cùng bộ nhớ tạm theo phiên gồm các sản phẩm bạn vừa xem, lần tìm kiếm gần nhất, kết quả tra cứu và thời gian chờ khi bị giới hạn tra cứu. Như mọi yêu cầu web, máy chủ nhận địa chỉ IP của bạn: mã của Rate.vn chỉ dùng nó trong bộ nhớ để giới hạn số yêu cầu mỗi phút, còn nhà cung cấp hosting giữ nhật ký truy cập thông thường trong thời gian ngắn.", + "privacyS5Desc": "Trên trang sản phẩm của 10 sàn được hỗ trợ, tiện ích gửi cho Rate.vn sàn, URL sản phẩm đã bỏ tham số, tên sản phẩm, mã sản phẩm và mã gian hàng, phiên bản đang chọn, giá bán và giá gốc để tra lịch sử giá; việc tra cứu này bật sẵn, chỉ đọc và không cần tài khoản. Nội dung bạn gõ vào ô tìm kiếm của tiện ích cũng được gửi để tra cứu. Chỉ khi bạn chọn \"Có\" ở trang hiện ra lúc cài đặt hoặc bật \"Đóng góp phát hiện sản phẩm mới (chỉ khi bạn bật)\" trong bảng bên cạnh (mặc định tắt), tiện ích còn gửi tin bán đó kèm số sao và số lượt đánh giá vào hàng đợi kiểm duyệt, lưu kèm thời điểm máy chủ nhận; máy chủ Rate.vn có thể kiểm tra lại tin bán đó trên sàn, và dữ liệu này không tự đổi giá công khai. Tiện ích không gửi tài khoản, tên, email hay cookie, không mở trang bạn không mở, và chỉ lưu trên trình duyệt của bạn địa chỉ máy chủ, lựa chọn có đóng góp hay không, cùng bộ nhớ tạm theo phiên gồm các sản phẩm bạn vừa xem, lần tìm kiếm gần nhất, kết quả tra cứu và thời gian chờ khi bị giới hạn tra cứu. Như mọi yêu cầu web, máy chủ nhận địa chỉ IP của bạn: mã của Rate.vn chỉ dùng nó trong bộ nhớ để giới hạn số yêu cầu mỗi phút, còn nhà cung cấp hosting giữ nhật ký truy cập thông thường trong thời gian ngắn.", "privacySide1Title": "Kiểm tra mã nguồn công khai", "privacySide1Desc": "Quy tắc phân quyền dữ liệu và cách lưu bình chọn đều công khai trong repository trên GitHub.", "privacySide1Btn": "Xem tài liệu kỹ thuật và API",