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:
parent
938387407f
commit
ba7a7c89a2
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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`,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
BIN
brand/offerlyst-brand-board.png
Normal file
BIN
brand/offerlyst-brand-board.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
brand/offerlyst-icon-1200.png
Normal file
BIN
brand/offerlyst-icon-1200.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 966 KiB |
BIN
brand/offerlyst-icon-256.png
Normal file
BIN
brand/offerlyst-icon-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
brand/offerlyst-icon-64.png
Normal file
BIN
brand/offerlyst-icon-64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
brand/offerlyst-logo-horizontal.png
Normal file
BIN
brand/offerlyst-logo-horizontal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 598 KiB |
BIN
brand/offerlyst-symbol-transparent.png
Normal file
BIN
brand/offerlyst-symbol-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 794 KiB |
@ -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"
|
||||||
|
|||||||
@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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. */
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
name = "Bundle Offers storefront"
|
name = "Offerlyst storefront"
|
||||||
type = "theme"
|
type = "theme"
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user