Demo page: the running product beside the case for it
A page to present this to a client. Left column is the real app in the phone frame — live, swipeable, the same deck and the same data, not a screenshot. Right column is what somebody needs in order to judge it: how to get in, what it does, what it is built on. Credentials, both sides - A one-sided demo of a two-sided marketplace shows half a product, so the panel carries a customer AND a tradesperson, each with a copy button. Reading a phone number off a screen into a form while a client watches is a small humiliation; mistyping one is a worse one. - dev-login grows from one pinned number to a named DEMO_ACCOUNTS list. The three guards are unchanged — not production, explicitly enabled, and the number must be on the list. Both accounts map to seeded users that already own jobs, conversations and a completed booking, so the screens have something in them rather than five empty states. - The page SAYS SO when sign-in is unavailable rather than letting somebody discover it mid-meeting. A fixed passcode is a login bypass and must never ship live; the honest fix for demoing against production is a real account and a real SMS, not a fourth flag. Content lives in arrays at the top of demo-panel, so adding a feature or swapping a dependency is one line and the layout is untouched. Four labelled placeholder slots — roadmap, pricing, metrics, case study — reserve the space and make it obvious what belongs where. A deliberate exception to DESIGN.md §1.0 and §4, which ban desktop layouts and marketing pages, and it is noted as one in the file. This is a frame around the running app for a laptop, not a product screen; the app inside is untouched and still mobile-only. Below `lg` the panel stacks under the phone, because a client who opens the link on their own phone should still be able to read it. Verified: both demo accounts complete the real OTP path end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,74 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { db, schema } from '@linkder/db';
|
||||
|
||||
/**
|
||||
* A fixed test account for local development.
|
||||
* Fixed sign-in for local development and for demonstrating the product.
|
||||
*
|
||||
* Signing in normally needs a real handset to receive a real SMS, which makes
|
||||
* the whole app untestable without a phone in your hand and Twilio credits. This
|
||||
* pins one number to one known code so `pnpm dev` is usable.
|
||||
* the whole app untestable without a phone in your hand and Twilio credits — and
|
||||
* makes it impossible to show somebody both sides of a two-sided marketplace in
|
||||
* a meeting. These accounts pin known numbers to one known code.
|
||||
*
|
||||
* THREE independent guards, because a login bypass reaching production is the
|
||||
* worst bug this codebase could ship:
|
||||
* THREE independent guards, unchanged by adding more accounts, because a login
|
||||
* bypass reaching production is the worst bug this codebase could ship:
|
||||
*
|
||||
* 1. NODE_ENV must not be 'production'.
|
||||
* 2. ALLOW_DEV_LOGIN must be explicitly 'true' — being in dev is not enough.
|
||||
* 3. The phone number must match exactly.
|
||||
* 3. The phone number must be one of the accounts listed below.
|
||||
*
|
||||
* Any one of them failing falls straight back to the real OTP path.
|
||||
* Any one of them failing falls straight back to the real OTP path. That is why
|
||||
* `/demo` says out loud that it only works off production: the honest fix for
|
||||
* demoing against a live deployment is a real account and a real SMS, not a
|
||||
* fourth flag that unlocks a bypass in production.
|
||||
*/
|
||||
const DEV_PHONE = '+34600000000';
|
||||
const DEV_CODE = '000000';
|
||||
|
||||
export interface DemoAccount {
|
||||
phone: string;
|
||||
code: string;
|
||||
/** Which side of the marketplace this account shows. */
|
||||
role: 'client' | 'pro';
|
||||
name: string;
|
||||
/** What the person presenting should point at once they are in. */
|
||||
blurb: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both sides of the market.
|
||||
*
|
||||
* A one-sided demo of a two-sided marketplace shows half a product: the
|
||||
* customer's deck is only interesting because a real tradesperson is on the
|
||||
* other end of it. These map to seeded accounts (`pnpm db:seed`) that already
|
||||
* own jobs, conversations and a completed booking, so the screens have something
|
||||
* in them rather than five empty states.
|
||||
*/
|
||||
export const DEMO_ACCOUNTS: readonly DemoAccount[] = [
|
||||
{
|
||||
phone: '+34600000000',
|
||||
code: DEV_CODE,
|
||||
role: 'client',
|
||||
name: 'Sofia Grau',
|
||||
blurb:
|
||||
'The customer. Has jobs in flight, a live conversation with a plumber, and one finished job waiting on a review.',
|
||||
},
|
||||
{
|
||||
phone: '+34610000001',
|
||||
code: DEV_CODE,
|
||||
role: 'pro',
|
||||
name: 'Marc Oliveras',
|
||||
blurb:
|
||||
'The tradesperson. Verified plumber — shows the card customers swipe, the verification state and the job side of chat.',
|
||||
},
|
||||
];
|
||||
|
||||
const DEMO_PHONES = DEMO_ACCOUNTS.map((a) => a.phone);
|
||||
|
||||
export function isDevLoginEnabled(): boolean {
|
||||
return process.env.NODE_ENV !== 'production' && process.env.ALLOW_DEV_LOGIN === 'true';
|
||||
}
|
||||
|
||||
export function isDevLoginPhone(phone: string): boolean {
|
||||
return isDevLoginEnabled() && phone === DEV_PHONE;
|
||||
return isDevLoginEnabled() && DEMO_PHONES.includes(phone);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,9 +86,11 @@ export async function pinDevLoginCode(phone: string): Promise<void> {
|
||||
await db
|
||||
.update(schema.verifications)
|
||||
.set({ value: `${DEV_CODE}:0` })
|
||||
.where(eq(schema.verifications.identifier, phone));
|
||||
.where(inArray(schema.verifications.identifier, [phone]))
|
||||
.catch(() => {});
|
||||
|
||||
console.info(`\n [dev login] ${DEV_PHONE} → code ${DEV_CODE}\n`);
|
||||
console.info(`\n [dev login] ${phone} → code ${DEV_CODE}\n`);
|
||||
}
|
||||
|
||||
export const DEV_LOGIN = { phone: DEV_PHONE, code: DEV_CODE } as const;
|
||||
/** Kept for callers that only ever wanted the customer account. */
|
||||
export const DEV_LOGIN = { phone: DEMO_ACCOUNTS[0]!.phone, code: DEV_CODE } as const;
|
||||
|
||||
Reference in New Issue
Block a user