Rename app to Offerlyst: Bundles & BOGO; add App Store icon and brand assets

Icon rebuilt as a full-bleed 1200x1200 square (Shopify rounds corners itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-14 09:03:32 -04:00
parent 938387407f
commit ba7a7c89a2
28 changed files with 29 additions and 26 deletions

View File

@ -2,5 +2,5 @@
APP_DOMAIN=bundles.example.com APP_DOMAIN=bundles.example.com
SHOPIFY_API_KEY=your_client_id SHOPIFY_API_KEY=your_client_id
SHOPIFY_API_SECRET=your_client_secret SHOPIFY_API_SECRET=your_client_secret
SHOPIFY_APP_HANDLE=bundle-offers SHOPIFY_APP_HANDLE=offerlyst
POSTGRES_PASSWORD=change-me-to-a-long-random-string POSTGRES_PASSWORD=change-me-to-a-long-random-string

View File

@ -1,4 +1,4 @@
# Bundle Offers web app (admin, app proxy, webhooks, scheduler). The Shopify extensions are deployed # Offerlyst web app (admin, app proxy, webhooks, scheduler). The Shopify extensions are deployed
# separately with `shopify app deploy`. # separately with `shopify app deploy`.
FROM node:22-alpine AS build FROM node:22-alpine AS build
RUN apk add --no-cache openssl RUN apk add --no-cache openssl

View File

@ -1,4 +1,6 @@
# Bundle Offers (Shopify app) # Offerlyst: Bundles & BOGO (Shopify app)
Brand assets: `brand/` (App Store icon `offerlyst-icon-1200.png`).
Public Shopify app that re-creates the ClicksToCart Odoo module `c2c_bundle_offers`: fixed bundles, mix & match Public Shopify app that re-creates the ClicksToCart Odoo module `c2c_bundle_offers`: fixed bundles, mix & match
and buy X get Y, applied by **one** automatic app discount whose function evaluates every running offer in and buy X get Y, applied by **one** automatic app discount whose function evaluates every running offer in

View File

@ -1,6 +1,6 @@
// orders/create -> analytics (F23, N02). The function's candidate message is the offer name, or "A + B" when // orders/create -> analytics (F23, N02). The function's candidate message is the offer name, or "A + B" when
// one line carries two offers; we attribute by name. If Shopify reports only the discount title // one line carries two offers; we attribute by name. If Shopify reports only the discount title
// ("Bundle Offers"), the usage is kept under that title. Verify the exact title on a dev store (README). // (DISCOUNT_TITLE, "Offerlyst bundles"), the usage is kept under that title. Verify the exact title on a dev store (README).
import db from "../db.server"; import db from "../db.server";
export interface OrderPayload { export interface OrderPayload {

View File

@ -6,7 +6,7 @@ import { compile, settingsOf, type CompileResult } from "./compile";
import { gql, ensureShop, type AdminLike } from "./shop.server"; import { gql, ensureShop, type AdminLike } from "./shop.server";
export const FUNCTION_HANDLE = "bundle-discount"; export const FUNCTION_HANDLE = "bundle-discount";
export const DISCOUNT_TITLE = "Bundle Offers"; export const DISCOUNT_TITLE = "Offerlyst bundles";
export const STOREFRONT_NAMESPACE = "bundle_offers"; export const STOREFRONT_NAMESPACE = "bundle_offers";
const CREATE = `#graphql const CREATE = `#graphql
@ -94,7 +94,7 @@ async function ensureDiscount(admin: AdminLike, shop: { id: string; discountNode
}, },
}); });
const err = userErrorText(data.discountAutomaticAppCreate.userErrors); const err = userErrorText(data.discountAutomaticAppCreate.userErrors);
if (err) throw new Error(`Could not create the Bundle Offers discount: ${err}`); if (err) throw new Error(`Could not create the Offerlyst discount: ${err}`);
const id = data.discountAutomaticAppCreate.automaticAppDiscount.discountId as string; const id = data.discountAutomaticAppCreate.automaticAppDiscount.discountId as string;
await db.shop.update({ where: { id: shop.id }, data: { discountNodeId: id } }); await db.shop.update({ where: { id: shop.id }, data: { discountNodeId: id } });
return { id, created: true }; return { id, created: true };

View File

@ -106,7 +106,7 @@ export function bundlesListLiquid(offers: OfferWithRelations[], shop: ShopCtx, l
return `<section class="bo-section"> return `<section class="bo-section">
<h1 class="bo-section__title">Bundle offers</h1> <h1 class="bo-section__title">Bundle offers</h1>
${body} ${body}
${shop.branding ? `<p class="bo-powered">Bundles by <a href="https://apps.shopify.com/bundle-offers" rel="nofollow">Bundle Offers</a></p>` : ""} ${shop.branding ? `<p class="bo-powered">Bundles by <a href="https://apps.shopify.com/offerlyst" rel="nofollow">Offerlyst</a></p>` : ""}
</section>`; </section>`;
} }

View File

@ -21,7 +21,7 @@ export default function App() {
return ( return (
<div className={styles.index}> <div className={styles.index}>
<div className={styles.content}> <div className={styles.content}>
<h1 className={styles.heading}>Bundle Offers: run all your festival deals at once</h1> <h1 className={styles.heading}>Offerlyst: run all your bundle deals at once</h1>
<p className={styles.text}> <p className={styles.text}>
Fixed bundles, mix & match and buy X get Y, applied automatically at checkout. Each item counts towards one offer, in the order you choose. Fixed bundles, mix & match and buy X get Y, applied automatically at checkout. Each item counts towards one offer, in the order you choose.
</p> </p>

View File

@ -78,7 +78,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
export default function Home() { export default function Home() {
const d = useLoaderData<typeof loader>(); const d = useLoaderData<typeof loader>();
const steps = [ const steps = [
{ done: d.setup.embedOn, label: "Turn on the Bundle Offers app embed", help: "Loads the storefront script and styles (cart hints, add bundle to cart).", action: <s-button href={d.embedLink} target="_blank">Open theme editor</s-button> }, { done: d.setup.embedOn, label: "Turn on the Offerlyst app embed", help: "Loads the storefront script and styles (cart hints, add bundle to cart).", action: <s-button href={d.embedLink} target="_blank">Open theme editor</s-button> },
{ done: d.menuLinked, label: "Link the bundles page in your menu", help: "Your bundles page is at /apps/bundles. Add it to the main menu (Online Store › Navigation), or place the “Bundles grid” block on any page.", action: <s-button href={`https://${d.shop.domain}/admin/menus`} target="_blank">Open navigation</s-button> }, { done: d.menuLinked, label: "Link the bundles page in your menu", help: "Your bundles page is at /apps/bundles. Add it to the main menu (Online Store › Navigation), or place the “Bundles grid” block on any page.", action: <s-button href={`https://${d.shop.domain}/admin/menus`} target="_blank">Open navigation</s-button> },
{ done: d.totalOffers > 0, label: "Create your first offer", help: "Start from scratch or from a festival template.", action: <s-button href="/app/offers/new">Create offer</s-button> }, { done: d.totalOffers > 0, label: "Create your first offer", help: "Start from scratch or from a festival template.", action: <s-button href="/app/offers/new">Create offer</s-button> },
{ done: d.running.length > 0, label: "Publish an offer and preview it", help: "Published offers within their dates run automatically in cart and checkout.", action: <s-button href={`https://${d.shop.domain}/apps/bundles`} target="_blank">View bundles page</s-button> }, { done: d.running.length > 0, label: "Publish an offer and preview it", help: "Published offers within their dates run automatically in cart and checkout.", action: <s-button href={`https://${d.shop.domain}/apps/bundles`} target="_blank">View bundles page</s-button> },
@ -86,7 +86,7 @@ export default function Home() {
const doneCount = steps.filter((s) => s.done).length; const doneCount = steps.filter((s) => s.done).length;
return ( return (
<s-page heading="Bundle Offers"> <s-page heading="Offerlyst">
<s-button slot="primary-action" variant="primary" href="/app/offers/new">Create offer</s-button> <s-button slot="primary-action" variant="primary" href="/app/offers/new">Create offer</s-button>
{d.shop.publishError ? ( {d.shop.publishError ? (

View File

@ -10,7 +10,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin, session } = await authenticate.admin(request); const { admin, session } = await authenticate.admin(request);
const shop = await syncShop(admin, session.shop); // refresh the plan from activeSubscriptions const shop = await syncShop(admin, session.shop); // refresh the plan from activeSubscriptions
const store = session.shop.replace(".myshopify.com", ""); const store = session.shop.replace(".myshopify.com", "");
const handle = process.env.SHOPIFY_APP_HANDLE || "bundle-offers"; const handle = process.env.SHOPIFY_APP_HANDLE || "offerlyst";
return { return {
current: shop.planCode, current: shop.planCode,
pricingUrl: `https://admin.shopify.com/store/${store}/charges/${handle}/pricing_plans`, pricingUrl: `https://admin.shopify.com/store/${store}/charges/${handle}/pricing_plans`,

View File

@ -1,7 +1,7 @@
// Public privacy policy (required for the App Store listing): https://<app domain>/privacy // Public privacy policy (required for the App Store listing): https://<app domain>/privacy
import type { MetaFunction } from "react-router"; import type { MetaFunction } from "react-router";
export const meta: MetaFunction = () => [{ title: "Privacy policy · FestiveBundle" }]; export const meta: MetaFunction = () => [{ title: "Privacy policy · Offerlyst" }];
const CONTACT = "info@metatroncubesolutions.com"; const CONTACT = "info@metatroncubesolutions.com";
@ -9,7 +9,7 @@ export default function Privacy() {
return ( return (
<main style={{ maxWidth: 720, margin: "48px auto", padding: "0 20px", font: "16px/1.6 system-ui, sans-serif", color: "#1a1a1a" }}> <main style={{ maxWidth: 720, margin: "48px auto", padding: "0 20px", font: "16px/1.6 system-ui, sans-serif", color: "#1a1a1a" }}>
<h1>Privacy policy</h1> <h1>Privacy policy</h1>
<p>Last updated: October 13, 2026. This policy explains what the FestiveBundle app (by Metatroncube Software Solutions) collects when a merchant installs it on a Shopify store.</p> <p>Last updated: October 14, 2026. This policy explains what the Offerlyst app (by Metatroncube Software Solutions) collects when a merchant installs it on a Shopify store.</p>
<h2>What we collect</h2> <h2>What we collect</h2>
<ul> <ul>

View File

@ -5,7 +5,7 @@ import db from "../db.server";
// The access token is already revoked here, so the app can't call the Admin API. Shopify removes the app's // The access token is already revoked here, so the app can't call the Admin API. Shopify removes the app's
// functions with the app, so the automatic discount stops applying, and it deletes app-owned ($app and // functions with the app, so the automatic discount stops applying, and it deletes app-owned ($app and
// app-data) metafields. We mark the shop and keep its data for 48 h, until shop/redact deletes it. // app-data) metafields. We mark the shop and keep its data for 48 h, until shop/redact deletes it.
// The in-app "Remove all Bundle Offers data" action (Settings) deletes the discount explicitly. // The in-app "Remove all Offerlyst data" action (Settings) deletes the discount explicitly.
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, session, topic } = await authenticate.webhook(request); const { shop, session, topic } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`); console.log(`Received ${topic} webhook for ${shop}`);

View File

@ -2,7 +2,7 @@ import type { ActionFunctionArgs } from "react-router";
import { authenticate } from "../shopify.server"; import { authenticate } from "../shopify.server";
import db from "../db.server"; import db from "../db.server";
// Mandatory GDPR webhooks. Bundle Offers stores no customer personal data: only order ids, order names and // Mandatory GDPR webhooks. Offerlyst stores no customer personal data: only order ids, order names and
// amounts for analytics (no names, emails or addresses). customers/data_request and customers/redact have // amounts for analytics (no names, emails or addresses). customers/data_request and customers/redact have
// nothing to return or erase; shop/redact (48 h after uninstall) deletes everything about the shop. // nothing to return or erase; shop/redact (48 h after uninstall) deletes everything about the shop.
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
brand/offerlyst-icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

View File

@ -28,7 +28,7 @@ services:
SHOPIFY_API_KEY: ${SHOPIFY_API_KEY:?set SHOPIFY_API_KEY in .env} SHOPIFY_API_KEY: ${SHOPIFY_API_KEY:?set SHOPIFY_API_KEY in .env}
SHOPIFY_API_SECRET: ${SHOPIFY_API_SECRET:?set SHOPIFY_API_SECRET in .env} SHOPIFY_API_SECRET: ${SHOPIFY_API_SECRET:?set SHOPIFY_API_SECRET in .env}
SHOPIFY_APP_URL: https://${APP_DOMAIN:?set APP_DOMAIN in .env} SHOPIFY_APP_URL: https://${APP_DOMAIN:?set APP_DOMAIN in .env}
SHOPIFY_APP_HANDLE: ${SHOPIFY_APP_HANDLE:-bundle-offers} SHOPIFY_APP_HANDLE: ${SHOPIFY_APP_HANDLE:-offerlyst}
SCOPES: read_products,write_discounts,read_discounts,read_orders,write_files,read_themes,write_app_proxy SCOPES: read_products,write_discounts,read_discounts,read_orders,write_files,read_themes,write_app_proxy
expose: expose:
- "3000" - "3000"

View File

@ -1,4 +1,4 @@
{ {
"name": "Bundle Offers", "name": "Offerlyst bundles",
"description": "Applies fixed, mix & match and buy X get Y bundle discounts. Each item counts towards one offer, in priority order." "description": "Applies fixed, mix & match and buy X get Y bundle discounts. Each item counts towards one offer, in priority order."
} }

View File

@ -1,4 +1,4 @@
# Bundle Offers: ONE automatic app discount per shop runs this function, which evaluates every running offer. # Offerlyst: ONE automatic app discount per shop runs this function, which evaluates every running offer.
# Rust (not JS): the JS build needed 39M instructions just to read a 200-line cart (limit 11M). See README. # Rust (not JS): the JS build needed 39M instructions just to read a 200-line cart (limit 11M). See README.
api_version = "2026-07" api_version = "2026-07"

View File

@ -1,4 +1,4 @@
/* Bundle Offers storefront styles. /* Offerlyst storefront styles.
Inherits the theme: fonts, text colour and background come from the page. Lines are a mix of the text Inherits the theme: fonts, text colour and background come from the page. Lines are a mix of the text
colour, so they work on light and dark themes alike. The merchant can set an accent and a radius (Settings). colour, so they work on light and dark themes alike. The merchant can set an accent and a radius (Settings).
The one decorative device is the coupon stub on the badge: festival offers as tear-off coupons. */ The one decorative device is the coupon stub on the badge: festival offers as tear-off coupons. */

View File

@ -1,4 +1,4 @@
/* Bundle Offers storefront script (app embed). Vanilla JS, no dependencies. /* Offerlyst storefront script (app embed). Vanilla JS, no dependencies.
* - converts shop-currency deal amounts when the customer shops in another currency * - converts shop-currency deal amounts when the customer shops in another currency
* - bundle pages (/apps/bundles/...): quantity steppers, "N of M chosen", add all items in one request * - bundle pages (/apps/bundles/...): quantity steppers, "N of M chosen", add all items in one request
* - refreshes cart hints after AJAX cart changes */ * - refreshes cart hints after AJAX cart changes */

View File

@ -27,7 +27,7 @@
{% schema %} {% schema %}
{ {
"name": "Bundle Offers", "name": "Offerlyst",
"target": "body", "target": "body",
"javascript": "bundle-offers.js", "javascript": "bundle-offers.js",
"stylesheet": "bundle-offers.css", "stylesheet": "bundle-offers.css",

View File

@ -17,7 +17,7 @@
<a class="bo-button" href="{{ routes.all_products_collection_url }}">{{ 'bundle_offers.grid.continue' | t }}</a> <a class="bo-button" href="{{ routes.all_products_collection_url }}">{{ 'bundle_offers.grid.continue' | t }}</a>
</div> </div>
{%- endif -%} {%- endif -%}
{%- if data.brand -%}<p class="bo-powered">Bundles by <a href="https://apps.shopify.com/bundle-offers" rel="nofollow">Bundle Offers</a></p>{%- endif -%} {%- if data.brand -%}<p class="bo-powered">Bundles by <a href="https://apps.shopify.com/offerlyst" rel="nofollow">Offerlyst</a></p>{%- endif -%}
</section> </section>
{% schema %} {% schema %}

View File

@ -1,2 +1,2 @@
name = "Bundle Offers storefront" name = "Offerlyst storefront"
type = "theme" type = "theme"

View File

@ -1,4 +1,4 @@
// Bundle Offers app database (brief §14). Postgres. // Offerlyst app database (brief §14). Postgres.
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }

View File

@ -1,7 +1,8 @@
# Bundle Offers — Shopify app configuration. # Offerlyst — Shopify app configuration.
# Run `npm run config:link` once to connect this folder to your app in the Partner Dashboard (fills client_id). # Run `npm run config:link` once to connect this folder to your app in the Partner Dashboard (fills client_id).
client_id = "" client_id = ""
name = "Bundle Offers" name = "Offerlyst: Bundles & BOGO"
handle = "offerlyst"
application_url = "https://example.com" application_url = "https://example.com"
embedded = true embedded = true