Checkout
Live path is Whish. Homepage/pricing CTAs go to /checkout?plan=. How this grants user_access and lands on dashboard: 00-how-everything-connects.md.
Plans (source lib/payments/catalog.ts; live overlays via getPaymentPlans() apply to homepage, pricing, and checkout charges):
| URL slug | Name | Price | Courses | Days |
|---|---|---|---|---|
focus | Focus | $10 | 1 | 120 |
balance | Balance | $17 | 2 | 120 |
fullload | Full Load | $25 | up to 6 | 120 |
crunch | Crunch | $14 | up to 4 | 21 |
PAGE: /checkout
Purpose: Pick courses, collect details, start Whish payment.
Access: logged-in. Missing/invalid ?plan= → /pricing. No session → /login?next=/checkout?plan=….
Entry points: Homepage Get Focus / Get Balance / Cover my semester / Prep for Finals; pricing same labels; course detail Get access → with &course={courses.id}; payment failure Return to checkout.
SECTION: Header
Copy: Logo alt Gradeful
Data shown: static
ELEMENT: Gradeful (logo)
Action: navigate
Destination: /
Writes to: none
Reads from: none
ELEMENT: Contact us
Action: navigate
Destination: /contact
Writes to: none
Reads from: none
ELEMENT: Back to plans
Action: navigate
Destination: /pricing
Writes to: none
Reads from: none
SECTION: Step indicator
Copy: Your course Your details Payment
Data shown: current step (1–2 in UI; payment is Whish, not a third in-app step)
SECTION: Step 1 — course picker
Copy by plan: coursePickerCopy in lib/payments/catalog.ts
Focus: Which course do you need? You're getting 1 course — choose the one you need most.
Balance: Which courses do you need? You're getting 2 courses — pick them now.
Full Load: Which courses are you taking? Pick up to 6 courses. The plan covers what you select — not the whole catalog.
Crunch: Which courses are you sitting? Pick up to 4 exam courses. You don't need to upload a schedule.
Search placeholder: Search by code — e.g. MATH201
Status: Loading courses… No courses found. Courses couldn't be loaded. Refresh and try again.
Data shown: courses (is_active=true) with course_packs and universities; optional initialCourseId from ?course=
ELEMENT: Search by code (input)
Action: toggle
Destination: none (filters list)
Writes to: none
Reads from: courses.code, courses.title
ELEMENT: Selected course chip (code + remove)
Action: toggle
Destination: none (deselect)
Writes to: none
Reads from: courses.code
ELEMENT: Course row (university, code, optional {n} cr, title)
Action: toggle
Destination: none (select/deselect)
Writes to: none
Reads from: courses.*, universities.short_name, credit hours on the course
Client limits: You can only pick 1 course on Focus You can only pick 2 courses on Balance You can pick up to {n} courses.
ELEMENT: Try again Action: toggle Destination: none (reload courses) Writes to: none Reads from: visible when load failed
ELEMENT: Continue
Action: toggle
Destination: step 2
Writes to: none (client state + sessionStorage gradeful.checkout.plan)
Reads from: selection validity
SECTION: Step 2 — details
Copy: Order summary shows plan name, included line, price, duration, selected courses. Empty No courses selected. No registered-credits or screenshot fields.
Included lines: Focus 1 course · all content types /1 course Full semester · 120 days; Balance 2 courses · all content types /2 courses; Full Load / Crunch The courses you pick · all content types /your courses; Crunch duration 21-day sprint.
Fields: Full name placeholder Your name (prefilled from profile, editable, does not write profiles); Signed-in email readonly This is the email on your Gradeful account. It can't be changed here. empty No email on this account; University hidden/read-only when profile already has AUB/LAU, else Select university / AUB / LAU / Other; WhatsApp (optional) +961 XX XXX XXX Optional, for account support. Payment uses the phone you enter on Whish.; institution email hidden when login already matches campus, else shown for receipt; checkbox personal tutoring-access sentence plus CHECKOUT_INDEPENDENCE_ACK.
Footnote: You'll continue to a secure payment page to complete the purchase. Need help?
Data shown: Auth email; selected courses; PLANS price
ELEMENT: Change courses Action: toggle Destination: step 1 Writes to: none Reads from: none
ELEMENT: Full name
Action: toggle
Destination: none
Writes to: orders.name
Reads from: none
Error: Full name is required.
ELEMENT: Signed-in email (readonly)
Action: none
Destination: none
Writes to: none
Reads from: Auth email
Error: Sign in to continue checkout.
ELEMENT: University
Action: toggle
Destination: none
Writes to: orders.university
Reads from: none
Error: Select your university.
ELEMENT: WhatsApp number
Action: toggle
Destination: none
Writes to: orders.whatsapp
Reads from: none
Error: WhatsApp number is required.
ELEMENT: Your uni email
Action: toggle
Destination: none
Writes to: orders.email (this is the uni email, not Auth email)
Reads from: none
Errors: Your uni email is required. Use a university email ending in .edu.
ELEMENT: I understand this material is for personal study only...
Action: toggle
Destination: none
Writes to: none (must be true to submit)
Reads from: none
Error: You must agree to continue.
ELEMENT: Continue to secure payment / Preparing secure payment…
Action: submit
Destination: Whish collectUrl (window.location.assign)
Writes to: orders via POST /api/payments/whish/create (user_id, name, email, university, plan, course_ids, registered_credits null, whatsapp, status=pending, payment_provider=whish, payment_status=pending, external_id, amount from live plan, currency=USD, collect_url, provider_metadata.durationDays, provider_metadata.catalogPrice)
Reads from: form + Auth session
Errors: An order was already submitted recently. Please wait and try again. Checkout is temporarily unavailable. Please try again shortly. Payment could not be started. Please try again. Unable to connect. Please check your connection. plus server error ≤160 chars. 401 → login with checkout next.
ELEMENT: Contact us
Action: navigate
Destination: /contact
Writes to: none
Reads from: none
API errors from create (also shown when returned): Sign in to continue checkout. Invalid request. Checkout details are incomplete. Use your AUB or LAU email for this payment. It is not your login. Selected courses are not available. One or more selected courses are not available for purchase. You already have access to those courses. Open them from My courses instead of paying again. Order could not be created. Try again. Something went wrong. Please try again. Entitlement: Remove duplicate courses and try again. Select at least one course for this plan. Select 1 course for this plan. Select {n} courses for this plan. You can pick up to {n} courses.
PAGE: /checkout/payment/success
Purpose: Return from Whish; poll until paid/failed/timeout. Arriving here does not mean paid by itself.
Access: logged-in (else login with next = this URL). Query orderId (UUID).
Entry points: Whish redirect after collect.
SECTION: PaymentReturnStatus (variant success)
Copy by state:
invalid-order: We couldn't find that payment Open checkout again from your plan, or return to pricing to start over.
paid: Payment confirmed Access is ready. You can open your courses now. Reference {8-char}
failed: Payment wasn't completed No access was granted. You can try payment again, or return to checkout to start over.
pending: We're confirming your payment Whish is still processing your payment. Keep this page open. If you haven't finished paying, you can return to Whish. This is not a failed payment.
unavailable (timeout): We couldn't confirm the payment yet We haven't confirmed this payment yet. That doesn't mean it failed. If you already paid, wait a minute and refresh, or contact support. If you haven't finished on Whish, you can continue there.
unavailable (other): We could not verify your payment right now. That doesn't mean it failed. Continue on Whish if you still need to pay, or contact support if you were charged.
checking: Checking payment status Hang tight — we're verifying with the payment provider.
Hint: Arriving here doesn't confirm payment by itself — we verify status securely.
Data shown: GET /api/payments/whish/status?orderId= → orders; reconcile may write user_access
ELEMENT: Return to pricing
Action: navigate
Destination: /pricing
Writes to: none
Reads from: none
ELEMENT: Go to my courses
Action: navigate
Destination: /dashboard
Writes to: none
Reads from: shown when accessGranted / paid
ELEMENT: Continue payment on Whish
Action: external link
Destination: Whish collectUrl / resumePaymentUrl
Writes to: none
Reads from: orders.collect_url
ELEMENT: Return to checkout
Action: navigate
Destination: /checkout with remembered plan
Writes to: none
Reads from: sessionStorage plan
ELEMENT: Contact Whish support
Action: external link
Destination: https://www.whish.money/contact-us
Writes to: none
Reads from: none
Poll may call reconcileWhishOrder → grantAccess → user_access. New grants do not insert schedule_verifications.
PAGE: /checkout/payment/failure Purpose: Same status poll as success; copy hint differs. Access: logged-in Entry points: Whish failure redirect.
SECTION: PaymentReturnStatus (variant failure)
Copy: Same state machine as success. Hint: Arriving here doesn't mean the payment permanently failed — we verify status securely.
Data shown: same as success
ELEMENT: same buttons as /checkout/payment/success