- name
- noon-shared
- description
- Common noon Seller Center mechanics โ login (OTP auto-fetch), page-structure URL map, My Catalog read access, common modals, button-click patterns, project-ID discovery. Prerequisite: every other noon-* skill (noon-listing, noon-fbn, noon-exports, noon-ads) expects this loaded for auth and shared UI patterns.
# Noon โ Shared (login, navigation, common patterns)
This skill covers what every noon Seller Center task needs:
authentication, the page-structure URL map, My Catalog read access,
and shared UI patterns (modals, button clicks). Operation-specific
skills (`noon-listing`, `noon-fbn`, `noon-exports`, `noon-ads`)
load this first.
## 1. Login โ Auto-fetch OTP When Email Is Bound
Noon uses **email OTP only** (no password). Before doing ANYTHING,
decide how the OTP will be retrieved โ do NOT bother the user if an
email MCP integration can do it for you.
### Decision flow (follow in order, do NOT skip steps)
1. Open the login page and look at the email it's about to send OTP to:
```bash
browser-use open "https://login.noon.partners/en/"
sleep 3 && browser-use state # find the email on the "We've sent..."
# screen OR the prefilled channelIdentifier
```
Ziniao often auto-fills or remembers a session. The page shows
`Log in with OTP sent to <email>`.
2. Check whether the task prompt includes a **"## Email System"**
section (the runner injects it when the store has connected
email accounts). Compare the bound emails to the Noon login
email shown on screen:
- **Match** โ fetch the OTP yourself via email MCP (Case A).
- **No email system section**, OR **emails do not match** โ
ask the user (Case B). Never guess โ mismatched emails mean
a different inbox.
### Case A โ Auto-fetch OTP via email MCP (preferred)
This works when the bound email address equals the Noon login
email. One round trip, no user prompt:
```python
# 1. Trigger immediate IMAP poll (don't wait 5 min for auto-sync):
vibe_seller_sync_email_now(account_email='<bound-email>')
# 2. Get the per-account DB path:
vibe_seller_email_info(store_id='<store-id>')
# Returns: { accounts: [{ email, db_path, ... }] }
```
Then read the latest Noon OTP email. The body is HTML โ the 6-digit
code sits on its own line inside a styled `<div>`. Extract it with
a regex on `body_html` (not `body_text`, which has escaped markup):
```bash
sqlite3 <db_path> "SELECT body_html FROM emails \
WHERE folder='INBOX' \
AND sender LIKE '%verify@noon.com%' \
AND subject='Verify your email' \
ORDER BY date DESC LIMIT 1" \
| python3 -c "
import sys, re
html = sys.stdin.read()
# The OTP is a standalone 6-digit number inside a styled div.
# Reject repeated digits (000000, 333333 etc. โ those are hex colors).
for m in re.findall(r'>\s*(\d{6})\s*<', html):
if len(set(m)) > 1:
print(m); break
"
```
Then fill it and continue:
```bash
browser-use input <otp-input> "<code>" # input targets by index
browser-use click <continue-btn>
browser-use state
browser-use click <maybe-later-btn> # dismiss passkey prompt
```
> **Don't ask the user at all in Case A.** The whole point of the
> binding is that the agent can finish login unattended.
### Case B โ Ask the user (only when Case A doesn't apply)
Trigger the OTP, then ask via `AskUserQuestion` (which requires
**2+ options**, not 1 โ a single-option call fails validation):
```
AskUserQuestion(questions=[{
"question": "I've triggered the Noon OTP email to <email>. Please enter the 6-digit code in the browser, then tell me when you're ready.",
"header": "OTP",
"options": [
{"label": "Entered, continue", "description": "I typed the OTP"},
{"label": "Didn't arrive", "description": "Resend or switch account"}
],
"multiSelect": false
}])
```
If the user can't receive the OTP, don't guess or retry โ escalate
and let them resolve.
### After login โ discover `{project_id}` from the URL
```bash
browser-use click <maybe-later-btn> # dismiss passkey prompt
# Redirects to welcome.noon.partners/... with project=PRJ{project_id}
```
**Do NOT ask the user for the project ID** if the store profile
(`stores/<slug>/STORE.md`) doesn't have it. Read it from the post-
login URL โ the welcome / store-home page always carries
`?project=PRJ{project_id}` (e.g. `NNNNNN`). Capture it once, then
reuse for the rest of the task. A project's countries share the same
numeric project ID; only the URL country suffix (`/en-<cc>/`) differs.
Optionally persist what you learned back to the store profile:
```
vibe_seller_write_workspace_file(
path="stores/<slug>/metadata.json",
content='{"platform_countries": {"noon": ["EG", "KW"]}, "noon_project_id": "<project_id>"}'
)
```
## 2. Page Structure Cheat Sheet
Most post-login URLs require `?project=PRJ{project_id}` (the login
and welcome pages are the only exceptions). `{project_id}` is the
numeric project identifier. The store identifier in path segments
like `STR{project_id}-N{CC}` reuses the same numeric value with
`STR` prefix and country suffix. Direct URL navigation works for
most pages; sidebar only for Support/Help.
| Page | URL |
|------|-----|
| Create listing | `noon-catalog.noon.partners/en/catalog/create` |
| Edit listing | `noon-catalog.noon.partners/en/catalog/{sku}/d?code={code}&offerTab=noon` |
| My Catalog | `noon-catalog.noon.partners/en/catalog` |
| Catalog Imports | `noon-catalog.noon.partners/en/imports` |
| Catalog Exports | `noon-catalog.noon.partners/en/exports` |
| FBN My ASN & Storage | `fbn.noon.partners/en-{cc}/asn` |
| FBN Create ASN | `fbn.noon.partners/en-{cc}/asn/createasn` |
| FBN My Inventory | `fbn.noon.partners/en-{cc}/inventory` |
| Sales | `reports.noon.partners/en/sales/` |
| Transaction View | `noon-payments.noon.partners/en/transaction-view` |
| Ad Manager | `admanager.noon.partners/en-{cc}/home?mpCode=noon` |
| Campaign Detail | `admanager.noon.partners/en-{cc}/campaign/details/{id}?mpCode=noon` |
| Create Campaign | `admanager.noon.partners/en-{cc}/campaign/start?mpCode=noon` |
| Vantage | `vantage.noon.partners/en/` |
## 3. My Catalog (read access)
**URL**: `https://noon-catalog.noon.partners/en/catalog?project=PRJ{project_id}`
Tabs: `noon` (default), `supermall`, `global`.
Each row has:
- Product title + Brand
- PSKU (Partner SKU) + SKU (noon ID), copyable
- Price, Sale/Promo badge
- Estimated Fees (FBN + FBP separately)
- Active Net Stock (FBN + FBP, links to inventory)
- Performance (Views, Units Sold, Sales)
- Seller Status toggle
- Live Status + "View Issues" link
Click the product title anchor to open the edit page (see
`noon-listing` skill).
For ad-tuning audits, this catalog read is the prerequisite step
that establishes "what the SKU actually is" before reading any
campaign โ see `noon-ads/references/ads-tuning.md`.
## 4. Common Modals
### Save Changes Warning
When leaving a page with unsaved changes:
> "Do you want to save the changes you made to this page?"
> [Discard Changes] [Save Changes]
Always click **Save Changes** unless intentionally reverting.
### Penalty Warning (ASN)
Before reserving FBN quota โ see `noon-fbn` skill. Click **Cancel**
to exit without commitment.
### Tour Modal
First-time visits show tours. Dismiss with "Skip for now".
### Hotjar Survey
NPS survey at bottom-right. Dismiss with `aria-label="Hide survey"`.
### Passkey Prompt (After Login)
Dismiss with "Maybe Later".
## 5. Button Clicking Patterns
### JS Click for Off-Screen Buttons
Listing wizard "Next" buttons are often outside viewport:
```bash
browser-use eval "
var r=''; document.querySelectorAll('button').forEach(function(b,i){
r+=i+':'+b.textContent.trim().substring(0,30)+':d='+b.disabled+' | ';
}); r
"
# Find the right index, then:
browser-use eval "document.querySelectorAll('button')[0].click()"
```
### Iframe Sidebar Clicks
Toolbar iframe is cross-origin; use `get bbox` on the parent
category button, then click below it at estimated offset:
```bash
browser-use click <category-btn> # expand
browser-use get bbox <category-btn> # returns {x, y, width, height}
# Sub-items are ~24-30px tall below, indented ~35px from left
browser-use click <x+35> <y+50> # first sub-item
```
## 6. Tips (general)
- **Always use direct URLs** for known pages (faster, more reliable).
- **Login OTP โ prefer email MCP over asking the user.** If the
store has a bound email account matching the Noon login email,
fetch the OTP yourself (Case A). Only ask when no binding applies.
- **Project ID is required** in the URL: `?project=PRJ{project_id}`.
- **Country switching varies by page** โ Sales/Transaction use bottom-left
store dropdown; Ad Manager uses URL country suffix; FBN paths embed
the country code directly. Check per-page.
- **Save changes before navigating** โ warning modal can silently
lose data if you click Discard.
- **`browser-use input <idx> <value>` vs `browser-use type <value>`.**
`input` targets an element by index; `type` types into whatever is
currently focused. They are not interchangeable โ `type <idx>
<value>` fails with a usage error.
- **`AskUserQuestion` needs 2+ options** โ a single-option call
fails schema validation; always give at least a confirm and a
"didn't work" option.
- **Captures โ `/tmp/<run-slug>/`.** Per-run live data goes to a
temp dir, never under `~/.vibe-seller/knowledge/`.
## See also
- `noon-listing` โ create / edit SKUs (price, stock, content)
- `noon-fbn` โ FBN/ASN, inventory, barcode print
- `noon-exports` โ Sales report, Transaction View, Catalog Exports
- `noon-ads` โ Ad Manager (campaigns, tuning, keyword research, negatives)
View on GitHub