Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/extension/CHROMEWEBSTORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions apps/extension/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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.
19 changes: 17 additions & 2 deletions apps/extension/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -53,23 +54,37 @@ 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,
});
await build({
...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,
});
fs.copyFileSync(
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'
Expand Down
33 changes: 33 additions & 0 deletions apps/extension/src/_locales/cs/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
33 changes: 33 additions & 0 deletions apps/extension/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
33 changes: 33 additions & 0 deletions apps/extension/src/_locales/vi/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
34 changes: 28 additions & 6 deletions apps/extension/src/background/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
import {
buildCrowdIngestPayload,
DEFAULT_API_BASE_URL,
isOnboardingSender,
isPrivilegedExtensionSender,
normalizeAllowedApiBaseUrl,
ONBOARDING_PAGE_PATH,
sanitizeExtensionProductSignal,
selectFreshestSessionSignal,
} from '../security-guards.js';
Expand Down Expand Up @@ -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<ExtensionSettings> = {};
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) => {
Expand Down Expand Up @@ -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',
Expand All @@ -577,6 +598,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
try {
const nextSettings: Partial<ExtensionSettings> = {};
if (
!fromOnboarding &&
typeof message.apiBaseUrl === 'string' &&
message.apiBaseUrl.trim()
) {
Expand Down
Loading
Loading