Overview
- Project
- Pro:Direct wishlist app
- Focus
- Storefront integration / API / Merchant analytics
- Stack
- Shopify / Gadget / TypeScript / React
- Role
- End-to-end delivery
A heart button is the small part of a wishlist. The more interesting questions come after the click: which size did someone save? What happens when they return? Does a product appearing in several lists mean several customers want it? And what can a merchant reasonably learn from that interest?
I built a wishlist app for Pro:Direct that connects the customer experience to merchant reporting. It spans the backend, a storefront integration with the theme, shareable lists, and an embedded Shopify admin. My role covered end-to-end delivery.
Why we built our own
We previously used a third-party wishlist app, but we weren't satisfied with it. Building our own gave us control over how the experience fitted into the storefront and how it could evolve.
That also meant owning the less visible work: keeping saved items consistent, handling retries, maintaining the data, and deciding what the reporting actually meant. Those decisions became as important as the interface.
Saving a variant, not just a product
Consider a customer saving a shirt in medium. Saving only the product would lose part of their decision. Each wishlist item therefore represents a Shopify variant, preserving the selected size and allowing a different size of the same product to be saved separately.
That choice carries through the interface. On a product card, the heart indicates that the product is saved in any size. On the product page, it follows the selected variant. In the wishlist, the saved size remains visible alongside the theme's existing quick-add controls.
A saved item is still an expression of interest, not a reservation. Availability can change. Choosing another size when adding to the cart should not silently rewrite what the customer originally saved.
A shared contract between app and theme
The app owns persistent lists and enforces access on the server. The theme owns how those lists feel to use, composing the experience from its existing cards, drawers, size selectors, translations, and cart behaviour.
This avoids making the wishlist feel like a separate product dropped into the storefront. It also creates a clear engineering boundary: the interface can evolve around a consistent API without taking responsibility for deciding who may access a list.
A local snapshot helps saved hearts appear promptly while the storefront refreshes from the server. Repeated requests are another practical concern: a slow response should not turn one save into several items, or one list creation into several lists. The backend recognises repeated operations so the customer's intent survives a retry.
Sharing without opening up editing
A wishlist can also be a selection to send to someone else. Customers can create an expiring, read-only link and revoke it later. The recipient sees the current selection without receiving permission to edit the owner's list.
That sounds like a small variation on the wishlist page, but it changes the product contract. Sharing needs a deliberately limited view of the list, with its own lifecycle, rather than exposing the customer's normal editing interface.
Making saved interest useful to merchants
One customer saving a shirt in three lists is a useful example of why a dashboard needs more than a count. There are three saved entries, but only one customer expressing interest. The demand report keeps those measures separate and ranks products by distinct customers saving them, with a drill-down into variants.
Current demand and historical activity answer different questions too. “What is saved now?” is a snapshot. “What has been gaining interest?” needs a history of saves and removals. Reconstructing that history from the items that happen to remain would lose the very changes the report is meant to explain.
The activity report uses recorded actions and the shop's timezone to build daily views. It makes incomplete coverage visible rather than presenting every day as equally complete. That matters when collection has only recently started or retained history no longer covers the whole reporting period.
What counts as a conversion?
For this app, the conversion report measures observed wishlist visits that lead to a successful add-to-cart event. It does not claim a checkout, a paid purchase, or proof that the wishlist caused a sale.
The report distinguishes adding directly from a wishlist from continuing through a product page. Repeated event deliveries should not inflate the result, and conflicting observations are excluded rather than turned into a confident-looking number. Recent visits are given time to complete before entering the reporting cohort.
Even an empty state needs a definition. No eligible visits means there is no rate to calculate; it does not mean a zero per cent conversion rate. Similarly, a failed report request is an error, not evidence that nobody saved anything.
These are small distinctions in the interface, but they determine whether a merchant can trust what the dashboard is saying.
Doing less work to answer the question
The reporting implementation aggregates demand in the database instead of fetching every wishlist item into the app to count it. Product details are then fetched in batches for the resulting rows. Search, pagination, and variant drill-down keep each request focused on the merchant's current question.
The admin follows the same principle. Refreshing an activity or conversion panel does not also refresh a hidden demand report. When a merchant changes selections quickly, an older response cannot overwrite the newer selection. Existing summary figures can remain visible during a refresh, while detail rows are shown only when they match the current view.
This work reduced unnecessary requests and made loading states more deliberate. It is an implementation improvement, rather than a claim about measured production speed or revenue.
Bringing it together
The result is a wishlist we own and maintain, from a customer's saved size to the merchant's view of demand.
The most interesting part was making those views agree without flattening their differences: a product is not a variant, a saved entry is not a unique customer, and an add to cart is not a purchase. Keeping those distinctions explicit shaped both the data model and the experience built on top of it.