|
| 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