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 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-16 09:07:22 -04:00
parent a5941a5149
commit 6061877ca1
4 changed files with 46 additions and 31 deletions

12
app/lib/billing.server.ts Normal file
View File

@ -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<boolean> {
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;
}

View File

@ -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") {

View File

@ -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<ReturnType<typeof login>> | 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";
return null;
/**
* 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;
}
export const loader = async ({ request }: LoaderFunctionArgs) => ({ error: message(await login(request)) });
export const action = async ({ request }: ActionFunctionArgs) => ({ error: message(await login(request)) });
return null;
};
export default function Login() {
const loaderData = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const error = actionData?.error ?? loaderData.error;
return (
<main style={{ maxWidth: 420, margin: "10vh auto", padding: 16, fontFamily: "system-ui, sans-serif" }}>
<h1>Log in to Datewise</h1>
<Form method="post" style={{ display: "grid", gap: 8 }}>
<label htmlFor="shop">Shop domain</label>
<input id="shop" name="shop" placeholder="your-store.myshopify.com" autoComplete="on" />
{error && <p role="alert">{error}</p>}
<button type="submit">Log in</button>
</Form>
<main style={{ maxWidth: 480, margin: "10vh auto", padding: 16, fontFamily: "system-ui, sans-serif", lineHeight: 1.5 }}>
<h1>Open Datewise from Shopify</h1>
<p>
Datewise runs inside your Shopify admin. Install it from the Shopify App Store, or open it from
<strong> Apps</strong> in your Shopify admin if it's already installed.
</p>
</main>
);
}

View File

@ -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