Skip to content

Commit 5a09394

Browse files
committed
docs(spec): mobile Share-to-shoplit (PWA share target) design
Free, mobile-first path to add products without the IP-blocked server fetch: installable PWA registers an Android Web Share Target; sharing a product hands shoplit link+title(+price), posted as explicit fields to the existing add API. v1 keeps images simple (optional URL + placeholder); photo upload is a documented next phase.
1 parent 159c5a5 commit 5a09394

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Mobile "Share to shoplit" (PWA Share Target) — Design
2+
3+
**Date:** 2026-05-24
4+
**Status:** Approved (ready for implementation plan)
5+
6+
## Problem
7+
8+
shoplit is mobile-first (Indian creators), but the desktop browser extension —
9+
the easy "add this product" path — can't exist on mobile (mobile Chrome has no
10+
extensions). The server-side fallback (paste a shared URL → OG-fetch the product
11+
page for title/image/price) is blocked: retailer bot-protection (Nykaa, Myntra,
12+
etc.) rejects the AWS host IP. So mobile creators are stuck typing each product's
13+
details by hand, one at a time.
14+
15+
## Constraints (decided during brainstorming)
16+
17+
1. **Zero recurring cost.** No residential/mobile proxy, no paid headless-render
18+
service. The server cannot be the thing that fetches retailer pages.
19+
2. **Mobile-first, ship fast.** Pure-web PWA over a native app. (Native Android
20+
WebView extractor was considered and deferred — heavier build, store review,
21+
no iOS coverage.)
22+
3. **v1 image handling stays simple.** No file upload/storage in v1. Optional
23+
image-URL field + graceful placeholder. Photo upload is an explicit next phase.
24+
25+
## Core idea
26+
27+
Make shoplit an **installable PWA that registers in the Android share sheet** via
28+
the Web Share Target API. Sharing a product from a shopping app hands shoplit the
29+
**link + title (+ price when present in the text)**. shoplit posts those *explicit*
30+
fields to the existing add-item API — it never fetches the retailer page, so the
31+
IP block is irrelevant.
32+
33+
Title and price come for free from the share payload. The image is the only piece
34+
that can't be auto-fetched for free on mobile web (cross-origin CORS blocks a web
35+
page from reading a retailer page, even from the user's own phone); v1 makes the
36+
image optional with a clean placeholder, and photo upload is the next phase.
37+
38+
## User flow
39+
40+
1. **One-time:** user installs shoplit to the home screen. The manifest declares a
41+
Web Share Target.
42+
2. In any shopping app (Nykaa/Myntra/Amazon/Flipkart/AJIO): **Share → shoplit.**
43+
Android opens `https://shoplit.in/add?title=…&text=…&url=…`.
44+
3. The `/add` screen parses the payload → clean product URL, title, price, retailer
45+
→ pre-fills a compact add form with a **cart picker** (defaults to the last-used
46+
cart, remembered in `localStorage`).
47+
4. User confirms → `POST /api/v1/carts/{id}/items` with explicit fields. On success:
48+
"Added ✓ — Add another / View cart."
49+
50+
## Components
51+
52+
New unless noted. Follows existing Next.js 14 App Router + Tailwind token
53+
conventions (ink/cream/paper/rule/muted/accent, font-serif=Fraunces).
54+
55+
- **`web/app/manifest.ts`** — Next metadata manifest route. Declares:
56+
- `name`, `short_name`, `start_url`, `display: standalone`, theme/background
57+
colors (brand tokens).
58+
- `icons`: 192×192 and 512×512 PNGs (generated from the existing logo/mark;
59+
`maskable` variant included).
60+
- `share_target`: `{ action: "/add", method: "GET", params: { title: "title",
61+
text: "text", url: "url" } }`.
62+
- **`web/public/sw.js`** + a small client registrar component — minimal service
63+
worker required for Android installability. Network-passthrough fetch handler;
64+
**no aggressive caching** of dynamic/app pages (avoid stale auth/content). Just
65+
enough to satisfy installability criteria.
66+
- **`web/lib/parse-share.ts`** — pure, unit-tested function
67+
`parseShare({ title?, text?, url? }) → { productUrl, title, priceText }`:
68+
- Extract the first retailer URL found across `url` + `text` (regex; unwrap
69+
common short links left as-is — the add API/redirect already handles them).
70+
- Title: prefer the `title` param; else take `text` with the URL(s) removed and
71+
known boilerplate stripped (e.g. "Check out this product I found on Nykaa:");
72+
else humanize the URL slug; else empty (user fills in).
73+
- Price: regex for ``/`Rs.`/`INR` amounts in `text`.
74+
- Retailer detection is NOT done here — the server's existing
75+
`ogfetch.RetailerFromURL` fills it from `original_url`.
76+
- **`web/app/add/page.tsx`** — the prefilled add screen. Client component:
77+
- Reads `searchParams`, runs `parseShare`.
78+
- Compact mobile form: title, price, product link, note, optional **Image URL**;
79+
**cart picker** (lists the user's carts; defaults to last-used from
80+
`localStorage`; offers "+ New cart" if none exist).
81+
- Submits to `POST /api/v1/carts/{id}/items` with explicit
82+
`title/price_text/image_url/original_url/note` (server fills `retailer`).
83+
- Success state: "Added ✓" with **Add another** (clears form, keeps cart) and
84+
**View cart** actions.
85+
- Doubles as the **manual / iOS entry point**: a "Paste a product link" textarea
86+
runs the same `parseShare`, so the screen is useful even without share-target.
87+
- **Auth return-to**`/add` requires a logged-in creator. If logged out: stash
88+
the shared params (sessionStorage) and pass a `next=/add?…` through the Google
89+
sign-in round trip so the shared data survives login and lands back on `/add`.
90+
91+
**No backend changes** beyond wiring the login `next=`/return-to (the add-item API
92+
already accepts explicit fields and detects the retailer).
93+
94+
## Image handling (v1)
95+
96+
- Optional "Image URL" field (prefilled if the share text happens to contain an
97+
image URL; usually empty).
98+
- Otherwise the existing gradient placeholder (CartCover / ProductCard already
99+
render a branded fallback for missing images) — degrades gracefully.
100+
101+
### Next phase (explicitly deferred, not in this plan)
102+
103+
Photo upload: store uploaded images on the EC2 disk (served free via Caddy/Next),
104+
with size/type limits and resize. This unlocks (a) "pick from gallery / take a
105+
photo" and (b) auto-capturing the product image when a shopping app attaches one
106+
to the share (POST share_target with `files`, service-worker-handled). Tracked as
107+
a follow-up after v1 ships.
108+
109+
## Platform coverage
110+
111+
- **Android** (most Indian creators): full share-sheet flow. ✅
112+
- **iOS**: Safari has no Web Share Target. `/add` works via the "Paste a product
113+
link" flow (same parser); installing gives an app icon. Documented, not broken.
114+
- **Desktop**: unchanged — the browser extension remains the path.
115+
116+
## Error handling
117+
118+
- No URL in the share payload → open `/add` with an empty form + the paste box.
119+
- Logged out → preserve shared data across Google login (return-to), then resume.
120+
- User has no carts → cart picker offers "+ New cart" (or a default cart prompt).
121+
- Add API failure → inline error, form state preserved, retry.
122+
123+
## Testing
124+
125+
- **Unit (vitest):** `parse-share.ts` against real share strings captured from
126+
Nykaa, Myntra, Amazon (incl. `amzn.in` short links), Flipkart, AJIO — asserting
127+
correct `productUrl`, `title`, `priceText` (incl. the boilerplate-strip cases).
128+
- **Manual:** install the PWA on Android; share a product from each retailer app;
129+
verify prefill + add. iOS paste-link path. Logged-out return-to round trip.
130+
Lighthouse "installable" check passes.
131+
132+
## Scope guard (YAGNI)
133+
134+
Not in v1: residential proxy, headless browser, file upload/image storage, native
135+
Android/iOS app, offline caching beyond installability.

0 commit comments

Comments
 (0)