From 6061877ca1ce8f1b66ef9e2a97fddc10b7eb7bc0 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Fri, 16 Oct 2026 09:07:22 -0400 Subject: [PATCH] fix(review): no shop-domain form on /auth/login; test charges on dev stores - /auth/login no longer asks for a myshopify domain (App Store 2.3.1). With ?shop= it still hands off to OAuth; otherwise it points to Shopify. - Billing API charges are test charges outside production and on partner development stores (shop.plan.partnerDevelopment), so reviewers can pick a paid plan. Real shops are charged as before. - SCREENCAST.md matches the recording. Co-Authored-By: Claude Opus 5.5 --- app/lib/billing.server.ts | 12 ++++++++++++ app/routes/app.settings.tsx | 10 ++++++---- app/routes/auth.login.tsx | 39 ++++++++++++++++--------------------- brand/SCREENCAST.md | 16 ++++++++++----- 4 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 app/lib/billing.server.ts diff --git a/app/lib/billing.server.ts b/app/lib/billing.server.ts new file mode 100644 index 0000000..87b396c --- /dev/null +++ b/app/lib/billing.server.ts @@ -0,0 +1,12 @@ +import { type AdminGraphql, gql } from "./guard.server"; + +/** + * Billing API charges are test charges outside production and on development stores + * (including App Store reviewers' stores), which can't take real charges. Real shops pay. + */ +export async function billingIsTest(admin: AdminGraphql): Promise { + if (process.env.NODE_ENV !== "production") return true; + const d = await gql<{ shop: { plan: { partnerDevelopment: boolean } } }>(admin, `#graphql + query DatewiseBillingMode { shop { plan { partnerDevelopment } } }`).catch(() => null); + return !!d?.shop.plan.partnerDevelopment; +} diff --git a/app/routes/app.settings.tsx b/app/routes/app.settings.tsx index fe88562..0fd2e4a 100644 --- a/app/routes/app.settings.tsx +++ b/app/routes/app.settings.tsx @@ -3,6 +3,7 @@ import { Form, useActionData, useLoaderData, useNavigation } from "react-router" import prisma from "../db.server"; import type { ReminderPolicy } from "../engine/types"; import { adminContext, formBool, formInt, formString } from "../lib/admin.server"; +import { billingIsTest } from "../lib/billing.server"; import { PLAN_GROWTH, PLAN_PRO } from "../shopify.server"; import { bumpConfig } from "../services/config.server"; import { encrypt } from "../services/crypto.server"; @@ -14,7 +15,7 @@ import { type CustomizationKind, findCustomizations, findGuard, gql, setCustomiz export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, admin, billing } = await adminContext(request); // Keep the stored plan in sync with Shopify billing. - const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: process.env.NODE_ENV !== "production" }); + const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: await billingIsTest(admin) }); const plan = check.appSubscriptions.find((s) => s.status === "ACTIVE")?.name ?? "free"; if (plan !== cfg.shop.plan) await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan } }); const guard = await findGuard(admin).catch(() => null); @@ -53,14 +54,15 @@ export const action = async ({ request }: ActionFunctionArgs) => { if (intent.startsWith("plan:")) { const plan = intent.slice(5); + const isTest = await billingIsTest(admin); if (plan === "free") { - const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: process.env.NODE_ENV !== "production" }); - for (const sub of check.appSubscriptions) await billing.cancel({ subscriptionId: sub.id, isTest: process.env.NODE_ENV !== "production", prorate: true }); + const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest }); + for (const sub of check.appSubscriptions) await billing.cancel({ subscriptionId: sub.id, isTest, prorate: true }); await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan: "free" } }); return { ok: true, message: "Switched to Starter" }; } // Redirects the merchant to Shopify's approval page. - await billing.request({ plan: plan as typeof PLAN_GROWTH, isTest: process.env.NODE_ENV !== "production", returnUrl: `https://${cfg.shop.domain}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/settings` }); + await billing.request({ plan: plan as typeof PLAN_GROWTH, isTest, returnUrl: `https://${cfg.shop.domain}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/settings` }); } if (intent === "guard") { diff --git a/app/routes/auth.login.tsx b/app/routes/auth.login.tsx index dc11f3a..90f2573 100644 --- a/app/routes/auth.login.tsx +++ b/app/routes/auth.login.tsx @@ -1,31 +1,26 @@ -import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; -import { Form, useActionData, useLoaderData } from "react-router"; -import { LoginErrorType } from "@shopify/shopify-app-react-router/server"; +import type { LoaderFunctionArgs } from "react-router"; import { login } from "../shopify.server"; -function message(errors: Awaited> | undefined) { - if (!errors || errors instanceof Response) return null; - if (errors.shop === LoginErrorType.MissingShop) return "Enter your shop domain to log in"; - if (errors.shop === LoginErrorType.InvalidShop) return "Enter a valid shop domain, like your-store.myshopify.com"; +/** + * Datewise is installed and opened from Shopify only (App Store requirement 2.3.1), so there is no + * shop-domain form here. When Shopify sends ?shop=, hand off to OAuth; otherwise explain where to go. + */ +export const loader = async ({ request }: LoaderFunctionArgs) => { + if (new URL(request.url).searchParams.get("shop")) { + const res = await login(request); + if (res instanceof Response) throw res; + } return null; -} - -export const loader = async ({ request }: LoaderFunctionArgs) => ({ error: message(await login(request)) }); -export const action = async ({ request }: ActionFunctionArgs) => ({ error: message(await login(request)) }); +}; export default function Login() { - const loaderData = useLoaderData(); - const actionData = useActionData(); - const error = actionData?.error ?? loaderData.error; return ( -
-

Log in to Datewise

-
- - - {error &&

{error}

} - -
+
+

Open Datewise from Shopify

+

+ Datewise runs inside your Shopify admin. Install it from the Shopify App Store, or open it from + Apps in your Shopify admin if it's already installed. +

); } diff --git a/brand/SCREENCAST.md b/brand/SCREENCAST.md index d133280..2d378fb 100644 --- a/brand/SCREENCAST.md +++ b/brand/SCREENCAST.md @@ -8,20 +8,26 @@ Claude drives the browser and shows an English caption at the bottom of each sce | # | Page | Caption | |---|---|---| | 1 | Shopify admin → Apps → Datewise (Today) | Datewise: Delivery & Pickup. Customers choose store pickup, local delivery or shipping, and a date and time. | -| 2 | Today: setup guide and capacity | The Today page shows a setup guide, today's bookings and capacity for the next 14 days. | +| 2 | Today: capacity | The Today page shows today's bookings and capacity for the next 14 days, per location. | | 3 | Locations & hours → Shop location | Each location has its own hours, time slots, preparation time, cutoffs and orders per slot. | | 4 | Theme editor: cart block + app embed | Add the scheduler to the cart page and switch on the app embed. This uses a theme app extension. | | 5 | Datewise Settings | Turn on the checkout guard, show the chosen time at checkout, and hide pay-in-person methods for shipped orders. | | 6 | Storefront cart | On the storefront, the customer chooses store pickup, a date and a time slot. | | 7 | Cart: confirmation stub | The chosen place is held for 10 minutes while the customer checks out. | -| 8 | Checkout → Pickup | At checkout, the pickup option shows the chosen date and time. | -| 9 | Checkout without a time | Without a chosen time, checkout is blocked. This works on every Shopify plan. | +| 8 | Checkout → Pickup | At checkout the customer picks up at the chosen location. Datewise writes the chosen date and time onto the order. | +| 9 | Checkout without a time | Without a chosen time, checkout is blocked. This works on every Shopify plan, including Buy it now. | | 10 | Thank-you page | The order is placed with a test payment. | | 11 | Datewise → Bookings | The booking appears in Datewise with its date, time, location and customer. | | 12 | Shopify order | The order shows the pickup time on the delivery line and in the order details. | | 13 | Rush control / Settings → plans | Paid plans add rush-day limits, a waitlist, run sheets and SMS reminders. Billing goes through Shopify. | | 14 | Today | Datewise: the right day, every order. | +## Recording notes + +- Recorded 2026-10-16: https://youtu.be/VZW767L6bNo (Unlisted). Scene order as recorded: 1–8, 10–12, 9, 13, 14. +- Blur the pickup location's address everywhere it appears (Locations list, checkout pickup card, thank-you page map and details). The thank-you page map shows the pin. +- Hide the old Metatron-delivery cart widget (`.dd-widget`) if that app is still active on the demo store. + ## YouTube - **Title:** Datewise: Delivery & Pickup for Shopify (app walkthrough) @@ -33,12 +39,12 @@ Claude drives the browser and shows an English caption at the bottom of each sce A walkthrough of Datewise: Delivery & Pickup, a Shopify app that lets customers choose store pickup, local delivery or shipping, and a date and time, in the cart. What this video shows: -• Home page with the setup guide and capacity view +• Home page with bookings and the capacity view • Location hours, time slots, preparation time and cutoffs • Adding the scheduler block and app embed in the theme editor • Checkout guard, date and time at checkout, and payment rules • Choosing a pickup date and time in the cart, with the place held during checkout -• Checkout showing the chosen time, and checkout blocked when no time is chosen +• Pickup at checkout, and checkout blocked when no time is chosen • The booking in Datewise and the time on the Shopify order • Plans, billed through Shopify