
Wishlist Save For Later
- 65 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Lets shoppers save products to a persistent wishlist, share it, and get back-in-stock or price-drop alerts on saved items.
About
Implements persistent wishlists for guest and logged-in users with shareable links, back-in-stock alerts, and move-to-cart flows. A developer uses it to reduce cart abandonment and capture demand for out-of-stock items.
- Persistent guest/auth wishlists merged on login
- Back-in-stock alerts and shareable wishlist links
Wishlist Save For Later by the numbers
- 65 all-time installs (skills.sh)
- Ranked #1,180 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill wishlist-save-for-laterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 65 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Lets shoppers save products to a persistent wishlist, share it, and get back-in-stock or price-drop alerts on saved items.
Files
Wishlist / Save for Later
Overview
Implement persistent wishlists that survive browser sessions for both authenticated and guest users. Includes shareable wishlist links, back-in-stock email alerts for out-of-stock wishlist items, and move-to-cart flows. Guest wishlists stored in localStorage are merged with the server-side list on login.
When to Use This Skill
- When shoppers want to save items for future purchase but are not ready to buy
- When building a feature to reduce cart abandonment by offering "save for later" on cart items
- When implementing back-in-stock notifications for high-demand products
- When the brand's social/sharing strategy should include wishlist sharing
Core Instructions
Step 1: Determine the merchant's platform and choose the right approach
| Platform | Recommended Approach | Why |
|---|---|---|
| Shopify | Install Wishlist Plus (free tier) or Growave (from $9/mo, includes wishlist + loyalty + reviews) | Wishlist Plus is the most installed wishlist app on Shopify — adds heart buttons to product and collection pages, persistent wishlists for logged-in users, guest wishlist via browser storage, and a shareable wishlist page |
| WooCommerce | Install YITH WooCommerce Wishlist (free) — the most widely used WooCommerce wishlist plugin | YITH Wishlist adds an "Add to Wishlist" button to product pages, creates a shareable wishlist page for each customer, handles guest wishlists via session, and includes "move to cart" functionality |
| BigCommerce | Enable the built-in Wishlists feature in Account → Wishlists — it's native to the platform; extend with Wishlist Plus app if needed | BigCommerce includes server-side wishlists for registered customers built in; guests need a third-party app or custom solution |
| Custom / Headless | Build with localStorage for guest users + server-side storage for authenticated users; merge on login | Full control over data model, sharing, and back-in-stock notifications; see implementation below |
Step 2: Set up wishlists on your platform
---
Shopify
Wishlist Plus (recommended — free tier available): 1. Install Wishlist Plus from the Shopify App Store 2. In the app dashboard:
- Set Guest wishlist: Enabled (uses browser storage for logged-out users)
- Set Auto-merge: Enabled (merges guest wishlist into account wishlist on login)
- Configure the heart button position: Above add to cart, Below add to cart, or On product image
3. The app automatically adds a heart/wishlist button to all product pages and collection cards 4. Configure Wishlist page URL (default: /pages/wishlist) in the app settings 5. Enable Email reminders: sends a reminder email when wishlist items go on sale or come back in stock 6. Add the wishlist link to your navigation:
- Go to Online Store → Navigation → Main menu
- Add a link pointing to
/pages/wishlist
Back-in-stock alerts:
- Wishlist Plus sends automatic back-in-stock emails when inventory is restored
- Configure the email template and timing in Wishlist Plus → Email Settings
---
WooCommerce
YITH WooCommerce Wishlist (free): 1. Install and activate from WordPress.org 2. Go to YITH → Wishlist → Settings → General:
- Set Wishlist page: create a new page with the
[yith_wcwl_wishlist]shortcode, then select it - Enable Share wishlist: lets customers share a public URL to their wishlist
- Enable Move to cart: shows a "Move to Cart" button on the wishlist page
- Configure Add to Wishlist button position: under Add to Cart, or via shortcode/widget
3. Under YITH → Wishlist → Settings → Guest Users:
- Enable Allow guests to use wishlist: stores in session/cookie
- Set Redirect after login: redirect to wishlist page so guest list merges automatically
4. The plugin creates a customer-specific wishlist page at /wishlist/?token=[user-token] for sharing
Back-in-stock with YITH:
- Install the companion plugin YITH WooCommerce Back In Stock Notifications (free)
- When a wishlist item goes out of stock, customers see an "Email me when available" option
- Configure the notification email template in the plugin settings
---
BigCommerce
Built-in Wishlists: 1. Wishlists are available to logged-in customers in their Account → Wishlists section 2. Customers can create multiple named wishlists (Birthday, Home, etc.) 3. To add "Add to Wishlist" buttons to product pages in Cornerstone:
- Go to Storefront → My Themes → Customize
- In Product Page, enable the Add to Wishlist button if not already visible
4. Shared wishlists: customers can set individual wishlists to Public in their account and share the URL
For guest wishlists: BigCommerce's built-in wishlist requires login. Install Wishlist Plus from the BigCommerce App Marketplace for guest wishlist support.
---
Custom / Headless
localStorage guest wishlist:
// lib/wishlistStore.js
const KEY = 'guest_wishlist';
export function getGuestWishlist() {
try { return JSON.parse(localStorage.getItem(KEY) ?? '[]'); } catch { return []; }
}
export function toggleGuestWishlist(item) {
const items = getGuestWishlist();
const exists = items.some(i => i.variantId === item.variantId);
const updated = exists
? items.filter(i => i.variantId !== item.variantId)
: [...items, { ...item, addedAt: Date.now() }];
try { localStorage.setItem(KEY, JSON.stringify(updated)); } catch {}
return updated;
}Merge guest wishlist on login:
export async function mergeGuestWishlistOnLogin() {
const guestItems = getGuestWishlist();
if (guestItems.length === 0) return;
await fetch('/api/wishlist/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: guestItems }),
});
localStorage.removeItem('guest_wishlist');
}
// Server: POST /api/wishlist/merge
// Insert only items not already in the user's server wishlistHeart button component:
function WishlistButton({ product, variant }) {
const { toggle, isWishlisted } = useWishlist();
const wishlisted = isWishlisted(variant.id);
return (
<button
onClick={() => toggle({ productId: product.id, variantId: variant.id })}
aria-label={wishlisted ? `Remove ${product.name} from wishlist` : `Add ${product.name} to wishlist`}
aria-pressed={wishlisted}
className={`wishlist-btn ${wishlisted ? 'active' : ''}`}>
<svg aria-hidden="true" viewBox="0 0 24 24"
fill={wishlisted ? 'currentColor' : 'none'} stroke="currentColor">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
</button>
);
}Back-in-stock notification trigger (run on inventory webhook or cron):
async function notifyBackInStock(variantId) {
const subscribers = await db.backInStockSubscriptions.findMany({
where: { variantId, notifiedAt: null },
});
for (const sub of subscribers) {
await emailService.send({
to: sub.email,
template: 'back-in-stock',
data: { productName: sub.productName, productUrl: sub.productUrl },
});
await db.backInStockSubscriptions.update({
where: { id: sub.id }, data: { notifiedAt: new Date() },
});
}
}Best Practices
- Optimistic UI for toggle — update the heart icon immediately on click; revert on API error to avoid perceived sluggishness
- Merge guest wishlists on login — nothing frustrates shoppers more than losing saved items after signing in
- Rate-limit back-in-stock emails — send at most one notification per subscriber per variant per restock event
- Show wishlist count in the header — a small badge count reinforces engagement
- Allow move-to-cart from wishlist — provide a "Move to Cart" button that adds the item and removes it from the wishlist atomically
- Expire guest wishlists — clear
localStorageentries older than 90 days to avoid showing discontinued products
Common Pitfalls
| Problem | Solution |
|---|---|
| Guest wishlist lost on login | Implement the merge flow immediately after authentication; trigger it in the post-login redirect |
| Back-in-stock email sends multiple times | Mark notifiedAt on the subscription record after sending; only notify subscribers where notifiedAt IS NULL |
| Wishlist heart causes full-page re-render | Manage wishlist state at a context level (React Context or Zustand) so only the heart button re-renders |
| Share link exposes private data | Render only product name, image, and price on shared wishlists — never addresses, notes, or account info |
| localStorage blocked in private browsing | Wrap all reads/writes in try/catch; fall back to in-memory storage for the current session |
Related Skills
- @recently-viewed-products
- @product-page-design
- @cart-abandonment-recovery
- @accessibility-commerce
{
"context": "Tests whether the agent implements the guest wishlist store following the skill's specific localStorage key, try/catch safety, deduplication logic, timestamp on add, 90-day expiry, and the correct merge-then-clear post-login flow.",
"type": "weighted_checklist",
"checklist": [
{
"name": "localStorage key",
"max_score": 10,
"description": "Uses the string 'guest_wishlist' as the localStorage key (not a different name like 'wishlist' or 'guestWishlist')"
},
{
"name": "try/catch on localStorage read",
"max_score": 10,
"description": "The function that reads from localStorage is wrapped in a try/catch block and returns an empty array (or equivalent empty state) in the catch branch"
},
{
"name": "Deduplication by variantId",
"max_score": 10,
"description": "addToGuestWishlist (or equivalent add function) checks whether an item with the same variantId already exists before adding it, and does NOT add a duplicate"
},
{
"name": "addedAt timestamp on add",
"max_score": 8,
"description": "When adding a new item, the function stores an addedAt timestamp (e.g. Date.now() or new Date()) on the item object"
},
{
"name": "90-day expiry logic",
"max_score": 10,
"description": "The store includes logic to remove or ignore wishlist items whose addedAt value is older than 90 days (approximately 90 * 24 * 60 * 60 * 1000 ms)"
},
{
"name": "Merge posts to server",
"max_score": 10,
"description": "mergeGuestWishlistOnLogin (or equivalent merge function) sends the locally-stored items to a server endpoint via a POST request with the items in the request body"
},
{
"name": "Merge clears local state",
"max_score": 10,
"description": "After a successful merge POST, the merge function clears the guest wishlist (saves an empty array to localStorage), not merely deleting the key"
},
{
"name": "Merge skips when empty",
"max_score": 8,
"description": "The merge function returns early (no fetch call) when the guest wishlist contains zero items"
},
{
"name": "removeFromGuestWishlist by variantId",
"max_score": 8,
"description": "The store exports a function to remove an item identified by variantId (filters the array by variantId)"
},
{
"name": "Test covers dedup and error",
"max_score": 16,
"description": "The test file includes at least one test that verifies duplicate items are NOT added, and at least one test that simulates a localStorage failure (e.g. by stubbing/mocking localStorage) without throwing"
}
]
}
Guest Wishlist Store Module
Problem/Feature Description
A headless e-commerce startup is building a new storefront where many visitors browse and save items before creating an account. The product team wants to ensure that products a visitor saves while browsing are not lost when they later sign up or log in — this has been a major complaint about the previous platform.
Your task is to build a self-contained JavaScript wishlist store module (lib/wishlistStore.js) that handles wishlist persistence for unauthenticated users, and a companion merge utility (lib/mergeWishlist.js) that runs immediately after a user authenticates. The merge utility should post the locally-stored items to a server endpoint and then clean up the local state.
The module will be used in a Next.js/React project but the store itself should be plain JavaScript with no framework dependencies. Another team is building the UI layer and they just need solid, well-named exports they can call.
Output Specification
Produce the following files:
lib/wishlistStore.js— The guest wishlist store with functions to read, add, remove, and save items to local browser storagelib/mergeWishlist.js— A merge utility that transfers locally-stored wishlist items to the server after login and clears local state on successlib/wishlistStore.test.js— Unit tests (using any test framework you prefer, or plain assertions) that demonstrate the key behaviors of the store, including deduplication and error handling
The test file should be runnable with node lib/wishlistStore.test.js (using Node's built-in assert module is fine) or with a standard test runner if you choose one. Include instructions in a comment at the top of the test file for how to run it.
{
"context": "Tests whether the agent designs the wishlist system following the skill's specific schema fields, nanoid-based slug generation, privacy-safe shared pages, upsert-based subscription deduplication, notifiedAt-based single-notification enforcement, and named-wishlist support in the data model.",
"type": "weighted_checklist",
"checklist": [
{
"name": "share_slug field in schema",
"max_score": 6,
"description": "The wishlists table (or model) includes a share_slug field (or shareSlug column) for the public sharing URL identifier"
},
{
"name": "is_public field in schema",
"max_score": 6,
"description": "The wishlists table (or model) includes an is_public (or isPublic) boolean field"
},
{
"name": "notify_back_in_stock field",
"max_score": 6,
"description": "The wishlist_items table (or model) includes a notify_back_in_stock (or notifyBackInStock) field"
},
{
"name": "Multiple named wishlists",
"max_score": 6,
"description": "The wishlists table (or model) includes a 'name' field, supporting multiple named wishlists per user"
},
{
"name": "nanoid for slug generation",
"max_score": 10,
"description": "The share link generation code uses nanoid (imported from 'nanoid') to create the share slug, specifically with a length of 10 characters"
},
{
"name": "Shared page field exclusion",
"max_score": 10,
"description": "The shared wishlist endpoint/handler (slug route) explicitly limits returned data to product name, image, and price — and includes a comment or note stating that user info, addresses, or notes are excluded"
},
{
"name": "Subscription upsert",
"max_score": 10,
"description": "The back-in-stock subscribe endpoint uses an upsert operation keyed on both variantId AND email (not a plain insert that could create duplicates)"
},
{
"name": "notifiedAt null filter",
"max_score": 12,
"description": "The back-in-stock notify function queries for subscribers where notifiedAt IS NULL (or equivalent), ensuring only un-notified subscribers receive emails"
},
{
"name": "Mark notifiedAt after send",
"max_score": 12,
"description": "After successfully sending an email to a subscriber, the notify function updates that subscriber's notifiedAt field to the current timestamp"
},
{
"name": "Guest email support",
"max_score": 8,
"description": "The subscribe endpoint accepts an email in the request body (for guests who are not authenticated) and falls back to the session email for authenticated users"
},
{
"name": "notifiedAt field in schema",
"max_score": 8,
"description": "The back-in-stock subscriptions table (or model) includes a notifiedAt field (can be nullable datetime)"
},
{
"name": "Design doc covers dedup and rate limiting",
"max_score": 6,
"description": "DESIGN.md (or equivalent) mentions both how duplicate subscriptions are prevented AND how repeated notifications for the same restock event are avoided"
}
]
}
Wishlist Sharing and Back-in-Stock Notification System
Problem/Feature Description
An outdoor gear retailer is launching a gift season campaign where shoppers can share wishlists with friends and family. Simultaneously, several high-demand items regularly go out of stock, and the merchandising team wants to re-engage customers as soon as inventory is replenished. They need both features designed cohesively so the database schema and API layer can support them long-term.
Your task is to design and implement the database schema, the API endpoints for enabling wishlist sharing and subscribing to back-in-stock alerts, and the notification dispatch function that fires when inventory is restocked. The solution should be production-ready: shareable links must be unguessable, shared wishlist pages should protect customer privacy, and the notification system must not spam subscribers with repeated emails for the same restock event.
Output Specification
Produce the following files:
schema.sql(orschema.prismaif using Prisma) — The complete database schema for the wishlist system, including all relevant tables and fieldsapi/wishlist/share.js— Endpoint to generate/enable a public sharing link for a wishlistapi/wishlist/shared/[slug].js— Endpoint (or page handler) that returns the data needed to render a shared wishlist; include a comment listing which fields are returned and which are intentionally excludedapi/back-in-stock/subscribe.js— Endpoint for a user or guest to subscribe to back-in-stock notifications for a specific product variantapi/back-in-stock/notify.js— Function/handler triggered when a variant comes back in stock; sends emails to eligible subscribersDESIGN.md— A brief design doc (bullet points are fine) explaining the sharing URL strategy, how duplicate subscriptions are prevented, and how the notification system avoids sending redundant emails
The files should use realistic placeholder calls (e.g. db.wishlists.update(...), emailService.send(...)) where actual database and email infrastructure would be wired in.
{
"context": "Tests whether the agent builds the useWishlist hook following the skill's patterns for dual auth/guest paths, optimistic UI with error revert, accessible button markup, correct SVG heart states, and context/store-level state management.",
"type": "weighted_checklist",
"checklist": [
{
"name": "userId param branching",
"max_score": 8,
"description": "useWishlist accepts a userId parameter and, when userId is truthy, fetches from the server (/api/wishlist or similar); when userId is falsy, reads from getGuestWishlist()"
},
{
"name": "DELETE for authenticated removal",
"max_score": 8,
"description": "When an authenticated user removes an item, the hook sends a DELETE request (not POST or PUT) to the items endpoint"
},
{
"name": "POST for authenticated add",
"max_score": 8,
"description": "When an authenticated user adds an item, the hook sends a POST request with the item in the request body"
},
{
"name": "Optimistic state update",
"max_score": 12,
"description": "The toggle function updates local state (items array) BEFORE the async API call resolves — the state change happens immediately on click"
},
{
"name": "Error revert",
"max_score": 12,
"description": "If the API call fails (catch block or error response), the hook reverts the state to its pre-toggle value"
},
{
"name": "aria-label with product name",
"max_score": 8,
"description": "WishlistButton sets aria-label dynamically to include the product name AND indicates the action ('Add ... to wishlist' or 'Remove ... from wishlist')"
},
{
"name": "aria-pressed attribute",
"max_score": 8,
"description": "WishlistButton includes aria-pressed={true} when the item is wishlisted and aria-pressed={false} (or aria-pressed={wishlisted}) when not"
},
{
"name": "SVG fill state",
"max_score": 8,
"description": "The SVG heart element uses fill=\"currentColor\" when wishlisted and fill=\"none\" (plus a stroke) when not wishlisted"
},
{
"name": "Context or store provider",
"max_score": 12,
"description": "Wishlist state is managed in a React Context Provider or a Zustand store — NOT stored as local state inside WishlistButton or individual product card components"
},
{
"name": "isWishlisted helper",
"max_score": 8,
"description": "The hook (or store) exposes an isWishlisted function or selector that accepts a variantId and returns a boolean"
},
{
"name": "loading state exposed",
"max_score": 8,
"description": "The hook exposes a loading boolean (or equivalent) as part of its return value"
}
]
}
Wishlist Toggle Hook and Heart Button Component
Problem/Feature Description
A fashion retailer's engineering team is building their new React storefront. The previous site had a wishlist button that was notoriously slow — clicking the heart icon would freeze the page for a moment because the state was stored in a monolithic top-level component. Shoppers complained and the conversion team's A/B tests showed that fixing the responsiveness would significantly lift engagement.
The new implementation needs a useWishlist React hook and a WishlistButton component. The hook must seamlessly handle both authenticated users (persisting to the backend) and unauthenticated guests (local browser storage), with the UI feeling instant regardless of network latency. The button must also meet accessibility standards because the retailer serves a diverse audience including screen reader users.
The team has agreed that wishlist state should live at the application level (not inside individual product cards) so that the heart icon on a product in a list view and the same product's detail page stay in sync without prop-drilling. They want to see this architectural choice reflected in the code.
Output Specification
Produce the following files:
hooks/useWishlist.js— The React hook for wishlist state managementcomponents/WishlistButton.jsx— The heart toggle button componentcontext/WishlistContext.jsx(orstore/wishlistStore.jsif using Zustand) — The application-level wishlist state provider or storeREADME.md— A short explanation (3–5 sentences) of how to integrate the WishlistButton into a product card and how to wrap the app with the provider/store
Assume that lib/wishlistStore.js (with getGuestWishlist, addToGuestWishlist, removeFromGuestWishlist) already exists and can be imported.
Input Files
The following file is provided as an input. Extract it before beginning.
=============== FILE: lib/wishlistStore.js =============== const GUEST_WISHLIST_KEY = 'guest_wishlist';
export function getGuestWishlist() { try { return JSON.parse(localStorage.getItem(GUEST_WISHLIST_KEY) ?? '[]'); } catch { return []; } }
export function saveGuestWishlist(items) { localStorage.setItem(GUEST_WISHLIST_KEY, JSON.stringify(items)); }
export function addToGuestWishlist(item) { const items = getGuestWishlist(); const exists = items.some(i => i.variantId === item.variantId); if (!exists) { saveGuestWishlist([...items, { ...item, addedAt: Date.now() }]); } }
export function removeFromGuestWishlist(variantId) { const items = getGuestWishlist().filter(i => i.variantId !== variantId); saveGuestWishlist(items); }
{
"name": "finsi/wishlist-save-for-later",
"version": "0.1.0",
"summary": "Persistent wishlists with sharing, back-in-stock alerts, and move-to-cart",
"skills": {
"wishlist-save-for-later": {
"path": "SKILL.md"
}
}
}