add this to live version

This commit is contained in:
Jun-te Kim 2026-02-07 19:38:11 +00:00
parent 35ced67475
commit ba48046f5b
13 changed files with 1281 additions and 427 deletions

View file

@ -41,25 +41,43 @@ Got you — heres a clean, founder-brain-friendly summary of **Stripe → Inv
--- ---
## ⚠️ Known issues youve already identified ## ✅ Recently fixed
* Xero contact creation: * **Xero contact creation** — Now checks for existing contacts by email first, reuses if found, only creates if missing
* **Stripe OAuth app reuse** — Added unique constraints on `userId` and `stripeAccountId` to prevent duplicate connections
* **Smart redirect flow** — Users are automatically routed based on connection state:
* Both connected → `/dashboard`
* Only Stripe → `/connect/xero`
* No connections → `/connect/stripe`
* **Connection visibility** — Dashboard now displays connected Stripe account ID and Xero tenant ID
* Need to **check for existing contact by email first** (currently can fail automation) ### Frontend Improvements Details
* Stripe OAuth:
**Smart Onboarding Flow**
* Automatic routing based on connection state
* Users never see unnecessary steps
* Seamless progression: Login → Stripe → Xero → Dashboard
**Dashboard Enhancements**
* Connected account visibility (Stripe account ID + Xero tenant)
* Account code configuration (sales + clearing accounts)
* Real-time save confirmation
* Clean, minimal UI
**Development Experience**
* Development mode fallback for webhook testing
* Comprehensive logging at each webhook stage
* Environment-aware configuration
---
## ⚠️ Known issues
* Currently creates a **new Stripe app every time** instead of reusing → needs fixing
* Account codes: * Account codes:
* Default behaviour needed on first Xero connection:
* `salesAccountCode = 200`
* `stripeClearingAccountCode = 610`
* Future: auto-detect or create a proper “Stripe Clearing” account and store it
* Missing UX guardrails: * Missing UX guardrails:
* No clear **pre-payment checklist** before enabling sync * No clear **pre-payment checklist** before enabling sync
* No UI yet to review/change account codes (fine for v1, but coming)
--- ---
@ -81,15 +99,11 @@ Got you — heres a clean, founder-brain-friendly summary of **Stripe → Inv
These unlock charging real money. These unlock charging real money.
* [ ] Fix Xero contact creation: * [x] ~~Fix Xero contact creation~~ ✅ DONE
* Check by email → reuse if exists → only create if missing * ~~Check by email → reuse if exists → only create if missing~~
* [ ] Fix Stripe OAuth app reuse (stop creating new apps) * [x] ~~Fix Stripe OAuth app reuse (stop creating new apps)~~ ✅ DONE
* [ ] Ensure default Xero account codes are set **on first connection** * [x] ~~Re-enable "mark invoice as paid" via Stripe Clearing once accounts are valid~~ ✅ DONE
* sales = 200
* clearing = 610
* [ ] Re-enable “mark invoice as paid” via Stripe Clearing once accounts are valid
> Outcome: rock-solid, boring, accountant-approved flow > Outcome: rock-solid, boring, accountant-approved flow
@ -97,18 +111,30 @@ These unlock charging real money.
### 2⃣ Add a tiny **pre-flight checklist UI** (not a full settings page) ### 2⃣ Add a tiny **pre-flight checklist UI** (not a full settings page)
* One screen before enabling sync: * [x] ~~Dashboard shows connected accounts~~ ✅ DONE
* ✔ Stripe connected * ~~Stripe account ID displayed~~
* ✔ Xero connected * ~~Xero tenant ID displayed~~
* ✔ VAT status detected * [x] ~~Smart redirect flow based on connection state~~ ✅ DONE
* ✔ Sales account code shown (read-only for now) * [ ] VAT status detection
* ✔ Stripe clearing account shown * [ ] Sales account code shown (editable)
* Even if its ugly — this prevents 80% of future support pain * [ ] Stripe clearing account shown (editable)
> Even basic connection visibility prevents 80% of future support pain
--- ---
### 3⃣ Switch from “design partner” → **first paid customer mode** ### 3⃣ Implement subscription billing (enables first paid customer)
* Integrate Stripe Billing for subscription management
* Add usage tracking (invoice count per month)
* Create pricing page and checkout flow
* Implement subscription status checks in webhook handler
* Remove "internal test" banner once billing is live
---
### 4⃣ Switch from "design partner" → **first paid customer mode**
* Pick **one**: * Pick **one**:
@ -116,16 +142,16 @@ These unlock charging real money.
* A cold UK Stripe + Xero business with obvious VAT needs * A cold UK Stripe + Xero business with obvious VAT needs
* Offer: * Offer:
* £10£30/month * £15/month Starter plan
* “Early access / founder pricing” * "Early access / founder pricing" (50% off for life)
* Manual support included * Manual support included
* Goal is **money changing hands**, not scale * Goal is **money changing hands**, not scale
> Youve said it yourself: getting paid energises you — lean into that. > You've said it yourself: getting paid energises you — lean into that.
--- ---
### 4️⃣ Do *targeted* cold outreach (low volume, high signal) ### 5️⃣ Do *targeted* cold outreach (low volume, high signal)
* 510 emails max, not a campaign * 510 emails max, not a campaign
* Target: * Target:
@ -135,21 +161,81 @@ These unlock charging real money.
* Clearly VAT-registered * Clearly VAT-registered
* Lead with: * Lead with:
* “I built this because my accountant hated existing tools” * "I built this because my accountant hated existing tools"
* Emphasise **audit-safe, VAT-correct invoices** * Emphasise **audit-safe, VAT-correct invoices**
* Not “automation”, not “syncing” * Not "automation", not "syncing"
--- ---
### 5⃣ Only then: small UX polish + automation ### 6⃣ Future UX polish + automation (after first paying customers)
* UI to review/change account codes * Auto-detect or create Stripe Clearing account in Xero
* Auto-detect or create Stripe Clearing account * Bulk historical invoice sync
* Invoice preview before creation
* Reduce manual fixes you find yourself repeating * Reduce manual fixes you find yourself repeating
* Nothing else until: * Nothing else until:
* You have **~35 paying users** * You have **~35 paying users**
* And theyre still using it after month 1 * And they're still using it after month 1
---
## 💳 SaaS Subscription Model (proposed)
### Pricing Tiers
**Starter — £15/month**
* Up to 50 invoices/month
* Stripe Payment Links + one-off payments
* Email support (48h response)
* Perfect for: Solo founders, side projects, small consultancies
**Professional — £35/month**
* Up to 200 invoices/month
* Everything in Starter, plus:
* Stripe Subscriptions support
* Priority email support (24h response)
* Custom account code mapping
* Perfect for: Growing businesses, SaaS products, agencies
**Business — £75/month**
* Unlimited invoices
* Everything in Professional, plus:
* Multi-currency support (planned)
* Dedicated Slack support
* Early access to new features
* Perfect for: Established businesses, high-volume merchants
### Implementation Notes
* **Billing via Stripe Checkout** (dogfooding our own product)
* **Monthly recurring subscriptions** with automatic renewal
* **14-day free trial** — no credit card required
* **Founder pricing lock-in** — First 50 customers get lifetime 50% off
* **Usage tracking** — Invoice count displayed in dashboard, soft warnings at 80% of limit
* **Graceful degradation** — Over-limit users get notified but sync continues (no hard cutoff)
### Revenue Model
* **Target: 100 paying customers in 6 months**
* 60% Starter (£900/mo)
* 30% Professional (£1,050/mo)
* 10% Business (£750/mo)
* Total: ~£2,700/mo MRR
* **Conservative burn**
* Hosting: £50/mo (Vercel + DB)
* Email: £10/mo (AWS SES)
* Support: Founder time only
* Net: ~£2,640/mo profit margin
### Next Steps for Monetization
1. Add Stripe Billing integration to the app
2. Implement usage tracking in webhook handler
3. Create pricing page on landing site
4. Add subscription management in dashboard
5. Enable payments and remove "internal test" banner
--- ---

View file

@ -1,4 +1,3 @@
// app/app/page.tsx
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@ -14,65 +13,119 @@ export default async function AppPage() {
redirect("/login"); redirect("/login");
} }
// Check if user has Stripe connection
const stripeConn = await db const stripeConn = await db
.select() .select()
.from(stripeAccounts) .from(stripeAccounts)
.where(eq(stripeAccounts.userId, user.id)) .where(eq(stripeAccounts.userId, user.id))
.limit(1); .limit(1);
// Check if user has Xero connection
const xeroConn = await db const xeroConn = await db
.select() .select()
.from(xeroConnections) .from(xeroConnections)
.where(eq(xeroConnections.userId, user.id)) .where(eq(xeroConnections.userId, user.id))
.limit(1); .limit(1);
// If both connected, go to dashboard
if (stripeConn.length > 0 && xeroConn.length > 0) { if (stripeConn.length > 0 && xeroConn.length > 0) {
redirect("/dashboard"); redirect("/dashboard");
} }
// If only Stripe connected, go to Xero connection
if (stripeConn.length > 0 && xeroConn.length === 0) { if (stripeConn.length > 0 && xeroConn.length === 0) {
redirect("/connect/xero"); redirect("/connect/xero");
} }
// Otherwise, show Stripe connection step
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<h1 className="text-2xl font-semibold"> {/* Navigation */}
Welcome{user.email ? `, ${user.email}` : ""} <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
</h1> <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
S2X
</div>
<div className="text-sm text-slate-600">
{user.email}
</div>
</div>
</nav>
{/* Progress */} <main className="pt-20">
<ol className="space-y-4"> <div className="max-w-2xl mx-auto px-6 py-16">
<li className="flex items-center gap-3"> {/* Welcome Section */}
<span className="text-green-600"></span> <div className="mb-12">
<span> <h1 className="text-4xl font-bold text-slate-900 mb-3">
Logged in as <strong>{user.email}</strong> Welcome, {user.email?.split("@")[0]}! 👋
</span> </h1>
</li> <p className="text-lg text-slate-600">
Let's set up your Stripe to Xero automation in just two steps.
</p>
</div>
<li className="flex items-center gap-3"> {/* Progress Visualization */}
<span className="text-blue-600"></span> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<span className="font-medium">Connect Stripe</span> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
</li>
<li className="text-gray-400"> <div className="space-y-4">
Xero will be connected after Stripe {/* Step 1 - Login */}
</li> <div className="flex gap-4">
</ol> <div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<div className="w-0.5 h-8 bg-green-200 my-1"></div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Email verified</p>
<p className="text-sm text-slate-600">{user.email}</p>
</div>
</div>
{/* Primary CTA */} {/* Step 2 - Stripe */}
<div className="pt-6"> <div className="flex gap-4">
<Link <div className="flex flex-col items-center">
href="/connect/stripe" <div className="w-10 h-10 rounded-full bg-blue-100 border-2 border-blue-600 flex items-center justify-center flex-shrink-0 font-semibold text-blue-600">
className="inline-block rounded bg-black text-white px-5 py-3" 2
> </div>
Connect Stripe <div className="w-0.5 h-8 bg-slate-200 my-1"></div>
</Link> </div>
</div> <div className="pt-1">
</main> <p className="font-semibold text-slate-900">Connect Stripe</p>
<p className="text-sm text-slate-600">Link your Stripe account to detect payments</p>
</div>
</div>
{/* Step 3 - Xero */}
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-slate-100 border-2 border-slate-300 flex items-center justify-center flex-shrink-0 font-semibold text-slate-600">
3
</div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Connect Xero</p>
<p className="text-sm text-slate-600">Link your Xero organisation to create invoices</p>
</div>
</div>
</div>
</div>
{/* CTA Section */}
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-8">
<h2 className="text-xl font-semibold text-slate-900 mb-3">
Ready to automate?
</h2>
<p className="text-slate-700 mb-6">
Connect your Stripe account to start detecting payments. You'll connect Xero next.
</p>
<Link
href="/connect/stripe"
className="inline-block px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Connect Stripe
</Link>
</div>
</div>
</main>
</div>
); );
} }

View file

@ -33,8 +33,35 @@ export default function AuthCallbackClient() {
}, [params, router]); }, [params, router]);
return ( return (
<main className="min-h-screen flex items-center justify-center"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
<p className="text-sm text-gray-500">Signing you in</p> <div className="text-center">
</main> {/* Animated loader */}
<div className="mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100">
<div className="relative w-12 h-12">
<div className="absolute inset-0 rounded-full border-2 border-slate-200"></div>
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-blue-600 border-r-blue-600 animate-spin"></div>
</div>
</div>
</div>
{/* Text */}
<h1 className="text-2xl font-bold text-slate-900 mb-2">Signing you in</h1>
<p className="text-slate-600">
Please wait while we authenticate your account
</p>
{/* Progress indication */}
<div className="mt-8">
<div className="inline-flex items-center gap-2">
<div className="flex gap-1">
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"></span>
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse" style={{ animationDelay: "0.2s" }}></span>
<span className="w-2 h-2 bg-blue-600 rounded-full animate-pulse" style={{ animationDelay: "0.4s" }}></span>
</div>
</div>
</div>
</div>
</div>
); );
} }

View file

@ -1,11 +1,3 @@
// app/connect/stripe/page.tsx
//
// STEP 2 — Connect Stripe
// Purpose:
// - Explain why Stripe access is needed
// - Provide a single, clear action
// - Feel safe, boring, and familiar (Zapier-style)
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -13,65 +5,132 @@ export default async function ConnectStripePage() {
const cookieStore = await cookies(); const cookieStore = await cookies();
const session = cookieStore.get("session"); const session = cookieStore.get("session");
// Safety: if not logged in, bounce to login
if (!session) { if (!session) {
redirect("/login"); redirect("/login");
} }
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
{/* -------------------------------------------------- {/* Navigation */}
Header <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
-------------------------------------------------- */} <div className="max-w-6xl mx-auto px-6 h-16 flex items-center">
<section> <div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
<h1 className="text-2xl font-semibold"> S2X
Connect Stripe </div>
</h1> </div>
</nav>
<p className="mt-3 text-gray-700"> <main className="pt-20">
We need read-only access to your Stripe account so we can <div className="max-w-2xl mx-auto px-6 py-16">
detect successful payments and automatically reconcile {/* Header */}
invoices in Xero. <div className="mb-12">
</p> <div className="inline-flex items-center gap-2 mb-4 px-3 py-1 bg-blue-50 rounded-full border border-blue-200">
</section> <span className="w-2 h-2 bg-blue-600 rounded-full"></span>
<span className="text-sm font-medium text-blue-700">Step 2 of 3</span>
</div>
{/* -------------------------------------------------- <h1 className="text-4xl font-bold text-slate-900 mb-4">
What will happen Connect Stripe
-------------------------------------------------- */} </h1>
<section>
<h2 className="text-lg font-medium">
What happens next
</h2>
<ul className="mt-3 space-y-2 list-disc list-inside text-gray-700"> <p className="text-lg text-slate-600">
<li>Youll be redirected to Stripe</li> We need read-only access to your Stripe account to monitor successful payments and automatically create invoices in Xero.
<li>Youll choose which Stripe account to connect</li> </p>
<li>Youll be sent back here once connected</li> </div>
</ul>
</section>
{/* -------------------------------------------------- {/* Two Column Layout */}
Trust / reassurance <div className="grid md:grid-cols-3 gap-8 mb-12">
-------------------------------------------------- */} {/* Left Column - Information */}
<section className="text-sm text-gray-600"> <div className="md:col-span-2">
<p> {/* What we access */}
We never see your passwords. <div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
<br /> <h2 className="text-lg font-semibold text-slate-900 mb-4">What we can access</h2>
Access can be revoked at any time from Stripe. <ul className="space-y-3">
</p> <li className="flex gap-3">
</section> <svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="font-medium text-slate-900">Payment events</p>
<p className="text-sm text-slate-600">Successful charges and refunds</p>
</div>
</li>
<li className="flex gap-3">
<svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="font-medium text-slate-900">Account information</p>
<p className="text-sm text-slate-600">Your account ID and basic metadata</p>
</div>
</li>
</ul>
</div>
{/* -------------------------------------------------- {/* What we cannot do */}
Primary action <div className="bg-amber-50 border border-amber-200 rounded-xl p-6 mb-6">
-------------------------------------------------- */} <h2 className="text-lg font-semibold text-slate-900 mb-4">What we cannot do</h2>
<section className="pt-4 border-t"> <ul className="space-y-2 text-sm text-slate-700">
<a <li className="flex gap-2">
href="/api/stripe/connect" <span className="text-amber-600"></span>
className="inline-block px-6 py-3 bg-black text-white rounded text-sm" <span>Charge customers or initiate refunds</span>
> </li>
Connect Stripe <li className="flex gap-2">
</a> <span className="text-amber-600"></span>
</section> <span>See or store sensitive payment information</span>
</main> </li>
<li className="flex gap-2">
<span className="text-amber-600"></span>
<span>Modify your Stripe settings or payouts</span>
</li>
</ul>
</div>
{/* How it works */}
<div className="bg-white border border-slate-200 rounded-xl p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">What happens next</h2>
<ol className="space-y-3">
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">1</span>
<span className="text-slate-700">You'll be securely redirected to Stripe</span>
</li>
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">2</span>
<span className="text-slate-700">Select which Stripe account to connect</span>
</li>
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">3</span>
<span className="text-slate-700">You'll be returned here to connect Xero</span>
</li>
</ol>
</div>
</div>
{/* Right Column - CTA Card */}
<div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border-2 border-blue-600 rounded-xl p-6 sticky top-24">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Ready?</h3>
<a
href="/api/stripe/connect"
className="block w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg text-center hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Connect Stripe
</a>
<p className="text-xs text-slate-600 mt-4 text-center">
You'll be redirected to Stripe's secure OAuth page
</p>
</div>
</div>
</div>
{/* Trust badges */}
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<p className="text-sm text-green-900">
<strong>🔒 Your security matters:</strong> We use OAuth, so you can revoke access from Stripe at any time. We never see or store your Stripe password.
</p>
</div>
</div>
</main>
</div>
); );
} }

View file

@ -2,43 +2,104 @@ import Link from "next/link";
export default function StripeSuccessPage() { export default function StripeSuccessPage() {
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<h1 className="text-2xl font-semibold"> {/* Navigation */}
Stripe connected 🎉 <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
</h1> <div className="max-w-6xl mx-auto px-6 h-16 flex items-center">
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
S2X
</div>
</div>
</nav>
<p className="text-gray-600"> <main className="pt-20">
Your Stripe account is now linked. We can now detect successful <div className="max-w-2xl mx-auto px-6 py-16">
payments and automatically reconcile invoices in Xero. {/* Success Animation */}
</p> <div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-6 animate-bounce">
<svg className="w-10 h-10 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
{/* Progress */} <h1 className="text-4xl font-bold text-slate-900 mb-3">
<ol className="space-y-4"> Stripe connected! 🎉
<li className="flex items-center gap-3"> </h1>
<span className="text-green-600"></span>
<span>Logged in</span>
</li>
<li className="flex items-center gap-3"> <p className="text-lg text-slate-600">
<span className="text-green-600"></span> Your Stripe account is now securely linked. We can detect successful payments and automatically reconcile invoices in Xero.
<span>Stripe connected</span> </p>
</li> </div>
<li className="flex items-center gap-3 text-blue-600"> {/* Progress Card */}
<span></span> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<span className="font-medium">Connect Xero</span> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
</li>
</ol>
{/* Primary CTA */} <div className="space-y-4">
<div className="pt-6 border-t"> {/* Step 1 - Login */}
<Link <div className="flex gap-4">
href="/connect/xero" <div className="flex flex-col items-center">
className="inline-block rounded bg-black text-white px-5 py-3" <div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
> <svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
Continue Connect Xero <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</Link> </svg>
</div> </div>
</main> <div className="w-0.5 h-8 bg-green-200 my-1"></div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Email verified</p>
<p className="text-sm text-slate-600">Authentication complete</p>
</div>
</div>
{/* Step 2 - Stripe */}
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<div className="w-0.5 h-8 bg-slate-200 my-1"></div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Stripe connected</p>
<p className="text-sm text-slate-600">Payment monitoring active</p>
</div>
</div>
{/* Step 3 - Xero */}
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-blue-100 border-2 border-blue-600 flex items-center justify-center flex-shrink-0 font-semibold text-blue-600">
3
</div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Connect Xero</p>
<p className="text-sm text-slate-600">One final step to enable automation</p>
</div>
</div>
</div>
</div>
{/* CTA */}
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-8">
<h2 className="text-xl font-semibold text-slate-900 mb-3">
Ready for the final step?
</h2>
<p className="text-slate-700 mb-6">
Now connect your Xero organisation to complete the setup and start automating invoices.
</p>
<Link
href="/connect/xero"
className="inline-block px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Continue to Xero
</Link>
</div>
</div>
</main>
</div>
); );
} }

View file

@ -1,9 +1,3 @@
// STEP 3 — Connect Xero
// Purpose:
// - Explain why Xero access is needed
// - Make the next step obvious
// - Match the Stripe connect page exactly
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -11,64 +5,141 @@ export default async function ConnectXeroPage() {
const cookieStore = await cookies(); const cookieStore = await cookies();
const session = cookieStore.get("session"); const session = cookieStore.get("session");
// Safety: if not logged in, bounce to login
if (!session) { if (!session) {
redirect("/login"); redirect("/login");
} }
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
{/* -------------------------------------------------- {/* Navigation */}
Header <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
-------------------------------------------------- */} <div className="max-w-6xl mx-auto px-6 h-16 flex items-center">
<section> <div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
<h1 className="text-2xl font-semibold"> S2X
Connect Xero </div>
</h1> </div>
</nav>
<p className="mt-3 text-gray-700"> <main className="pt-20">
We need access to your Xero organisation so we can automatically <div className="max-w-2xl mx-auto px-6 py-16">
create invoices and mark them as paid when Stripe payments succeed. {/* Header */}
</p> <div className="mb-12">
</section> <div className="inline-flex items-center gap-2 mb-4 px-3 py-1 bg-blue-50 rounded-full border border-blue-200">
<span className="w-2 h-2 bg-blue-600 rounded-full"></span>
<span className="text-sm font-medium text-blue-700">Step 3 of 3</span>
</div>
{/* -------------------------------------------------- <h1 className="text-4xl font-bold text-slate-900 mb-4">
What will happen Connect Xero
-------------------------------------------------- */} </h1>
<section>
<h2 className="text-lg font-medium">
What happens next
</h2>
<ul className="mt-3 space-y-2 list-disc list-inside text-gray-700"> <p className="text-lg text-slate-600">
<li>Youll be redirected to Xero</li> We need access to your Xero organisation to automatically create invoices and mark them as paid when Stripe payments succeed.
<li>Youll choose which organisation to connect</li> </p>
<li>Youll be sent back here once connected</li> </div>
</ul>
</section>
{/* -------------------------------------------------- {/* Two Column Layout */}
Trust / reassurance <div className="grid md:grid-cols-3 gap-8 mb-12">
-------------------------------------------------- */} {/* Left Column - Information */}
<section className="text-sm text-gray-600"> <div className="md:col-span-2">
<p> {/* What we do */}
We never see your Xero password. <div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
<br /> <h2 className="text-lg font-semibold text-slate-900 mb-4">What we can create</h2>
Access can be revoked at any time from Xero. <ul className="space-y-3">
</p> <li className="flex gap-3">
</section> <svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="font-medium text-slate-900">Invoices</p>
<p className="text-sm text-slate-600">Sales invoices matched to Stripe payments</p>
</div>
</li>
<li className="flex gap-3">
<svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="font-medium text-slate-900">Credit notes</p>
<p className="text-sm text-slate-600">For Stripe refunds and chargebacks</p>
</div>
</li>
<li className="flex gap-3">
<svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<div>
<p className="font-medium text-slate-900">Journal entries</p>
<p className="text-sm text-slate-600">For Stripe fees and reconciliation</p>
</div>
</li>
</ul>
</div>
{/* -------------------------------------------------- {/* Important notes */}
Primary action <div className="bg-purple-50 border border-purple-200 rounded-xl p-6 mb-6">
-------------------------------------------------- */} <h2 className="text-lg font-semibold text-slate-900 mb-4">Important notes</h2>
<section className="pt-4 border-t"> <ul className="space-y-2 text-sm text-slate-700">
<a <li className="flex gap-2">
href="/api/xero/connect" <span className="text-purple-600 font-bold"></span>
className="inline-block px-6 py-3 bg-black text-white rounded text-sm" <span>We only create invoices; we never modify or delete them</span>
> </li>
Connect Xero <li className="flex gap-2">
</a> <span className="text-purple-600 font-bold"></span>
</section> <span>All invoices are created as draft and marked as paid automatically</span>
</main> </li>
<li className="flex gap-2">
<span className="text-purple-600 font-bold"></span>
<span>VAT is handled correctly based on your Stripe data</span>
</li>
</ul>
</div>
{/* How it works */}
<div className="bg-white border border-slate-200 rounded-xl p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">What happens next</h2>
<ol className="space-y-3">
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">1</span>
<span className="text-slate-700">You'll be securely redirected to Xero</span>
</li>
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">2</span>
<span className="text-slate-700">Select which organisation to connect</span>
</li>
<li className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-600">3</span>
<span className="text-slate-700">Your dashboard will be ready to configure</span>
</li>
</ol>
</div>
</div>
{/* Right Column - CTA Card */}
<div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border-2 border-blue-600 rounded-xl p-6 sticky top-24">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Almost there!</h3>
<a
href="/api/xero/connect"
className="block w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg text-center hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Connect Xero
</a>
<p className="text-xs text-slate-600 mt-4 text-center">
You'll be redirected to Xero's secure OAuth page
</p>
</div>
</div>
</div>
{/* Trust badges */}
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<p className="text-sm text-green-900">
<strong>🔒 Your security matters:</strong> We use OAuth, so you can revoke access from Xero at any time. We never see or store your Xero password.
</p>
</div>
</div>
</main>
</div>
); );
} }

View file

@ -2,43 +2,125 @@ import Link from "next/link";
export default function XeroSuccessPage() { export default function XeroSuccessPage() {
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<h1 className="text-2xl font-semibold"> {/* Navigation */}
Xero connected 🎉 <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
</h1> <div className="max-w-6xl mx-auto px-6 h-16 flex items-center">
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
S2X
</div>
</div>
</nav>
<p className="text-gray-600"> <main className="pt-20">
Your Xero organisation is now linked. We can now automatically <div className="max-w-2xl mx-auto px-6 py-16">
create invoices and mark them as paid when Stripe payments succeed. {/* Success Animation */}
</p> <div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-6 animate-bounce">
<svg className="w-10 h-10 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
{/* Progress */} <h1 className="text-4xl font-bold text-slate-900 mb-3">
<ol className="space-y-4"> All set! 🚀
<li className="flex items-center gap-3"> </h1>
<span className="text-green-600"></span>
<span>Logged in</span>
</li>
<li className="flex items-center gap-3"> <p className="text-lg text-slate-600">
<span className="text-green-600"></span> Your Xero organisation is now securely linked. We can now automatically create invoices and mark them as paid when Stripe payments succeed.
<span>Stripe connected</span> </p>
</li> </div>
<li className="flex items-center gap-3"> {/* Progress Card */}
<span className="text-green-600"></span> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<span>Xero connected</span> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup complete</h2>
</li>
</ol>
{/* Primary CTA */} <div className="space-y-4">
<div className="pt-6 border-t"> {/* Step 1 - Login */}
<Link <div className="flex gap-4">
href="/dashboard" <div className="flex flex-col items-center">
className="inline-block rounded bg-black text-white px-5 py-3" <div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
> <svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
Go to dashboard <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</Link> </svg>
</div> </div>
</main> <div className="w-0.5 h-8 bg-green-200 my-1"></div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Email verified</p>
<p className="text-sm text-slate-600">Authentication complete</p>
</div>
</div>
{/* Step 2 - Stripe */}
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<div className="w-0.5 h-8 bg-green-200 my-1"></div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Stripe connected</p>
<p className="text-sm text-slate-600">Payment monitoring active</p>
</div>
</div>
{/* Step 3 - Xero */}
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
<svg className="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
<div className="pt-1">
<p className="font-semibold text-slate-900">Xero connected</p>
<p className="text-sm text-slate-600">Invoice automation enabled</p>
</div>
</div>
</div>
</div>
{/* What's next */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-8 mb-8">
<h2 className="text-lg font-semibold text-slate-900 mb-4">What happens now</h2>
<ul className="space-y-3">
<li className="flex gap-3">
<span className="text-blue-600 font-bold text-lg"></span>
<span className="text-slate-700">Configure your Xero account codes in the dashboard</span>
</li>
<li className="flex gap-3">
<span className="text-blue-600 font-bold text-lg"></span>
<span className="text-slate-700">Make a test Stripe payment to verify everything works</span>
</li>
<li className="flex gap-3">
<span className="text-blue-600 font-bold text-lg"></span>
<span className="text-slate-700">Watch as invoices automatically appear in Xero</span>
</li>
</ul>
</div>
{/* CTA */}
<div className="bg-gradient-to-br from-green-50 to-green-100 border border-green-200 rounded-xl p-8">
<h2 className="text-xl font-semibold text-slate-900 mb-3">
Go to your dashboard
</h2>
<p className="text-slate-700 mb-6">
Configure your account codes and start using the automation.
</p>
<Link
href="/dashboard"
className="inline-block px-6 py-3 bg-gradient-to-r from-green-600 to-green-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-green-700 hover:to-green-800 transition"
>
Open dashboard
</Link>
</div>
</div>
</main>
</div>
); );
} }

View file

@ -4,12 +4,12 @@ import { useEffect, useState } from "react";
export default function DashboardPage() { export default function DashboardPage() {
const [salesAccountCode, setSalesAccountCode] = useState(""); const [salesAccountCode, setSalesAccountCode] = useState("");
const [stripeClearingAccountCode, setStripeClearingAccountCode] = const [stripeClearingAccountCode, setStripeClearingAccountCode] = useState("");
useState("");
const [stripeAccountId, setStripeAccountId] = useState(""); const [stripeAccountId, setStripeAccountId] = useState("");
const [xeroTenantId, setXeroTenantId] = useState(""); const [xeroTenantId, setXeroTenantId] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
@ -26,88 +26,195 @@ export default function DashboardPage() {
async function save() { async function save() {
setSaved(false); setSaved(false);
setError(null);
await fetch("/api/dashboard/xero-settings", { try {
method: "POST", await fetch("/api/dashboard/xero-settings", {
headers: { "Content-Type": "application/json" }, method: "POST",
body: JSON.stringify({ headers: { "Content-Type": "application/json" },
salesAccountCode, body: JSON.stringify({
stripeClearingAccountCode, salesAccountCode,
}), stripeClearingAccountCode,
}); }),
});
setSaved(true); setSaved(true);
setTimeout(() => setSaved(false), 3000);
} catch (err) {
setError("Failed to save settings. Please try again.");
}
} }
if (loading) return <p>Loading</p>; if (loading) {
return (
return ( <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
<div className="max-w-xl space-y-6"> <div className="text-center">
<h1 className="text-2xl font-semibold"> <div className="inline-flex items-center justify-center w-12 h-12 mb-4">
Stripe Xero settings <span className="w-3 h-3 bg-blue-600 rounded-full animate-spin"></span>
</h1> </div>
<p className="text-slate-600">Loading your settings</p>
{/* Connected Accounts */}
<div className="border rounded p-4 bg-gray-50 space-y-3">
<h2 className="font-medium text-sm text-gray-600">Connected Accounts</h2>
<div className="space-y-2 text-sm">
<p>
<span className="font-medium">Stripe:</span>{" "}
<code className="bg-white px-2 py-1 rounded text-xs">
{stripeAccountId || "Not connected"}
</code>
</p>
<p>
<span className="font-medium">Xero:</span>{" "}
<code className="bg-white px-2 py-1 rounded text-xs">
{xeroTenantId || "Not connected"}
</code>
</p>
</div> </div>
</div> </div>
);
}
<div> return (
<label className="block text-sm font-medium"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
Sales account code {/* Navigation */}
</label> <nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
<input <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
className="mt-1 w-full border px-3 py-2" <div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
value={salesAccountCode} S2X
onChange={(e) => setSalesAccountCode(e.target.value)} </div>
/> <div className="text-sm text-slate-600">Dashboard</div>
<p className="text-sm text-zinc-500"> </div>
Used on invoice line items </nav>
</p>
</div>
<div> <main className="pt-20">
<label className="block text-sm font-medium"> <div className="max-w-4xl mx-auto px-6 py-16">
Stripe clearing account code {/* Header */}
</label> <div className="mb-12">
<input <h1 className="text-4xl font-bold text-slate-900 mb-3">Dashboard</h1>
className="mt-1 w-full border px-3 py-2" <p className="text-lg text-slate-600">
value={stripeClearingAccountCode} Configure your Xero account codes and manage your automation settings.
onChange={(e) => </p>
setStripeClearingAccountCode(e.target.value) </div>
}
/>
<p className="text-sm text-zinc-500">
Receives Stripe payments
</p>
</div>
<button <div className="grid lg:grid-cols-3 gap-8">
onClick={save} {/* Main Content */}
className="rounded bg-black px-4 py-2 text-white" <div className="lg:col-span-2 space-y-6">
> {/* Connected Accounts */}
Save <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
</button> <h2 className="text-lg font-semibold text-slate-900 mb-4">Connected Accounts</h2>
<div className="space-y-4">
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg border border-blue-200">
<p className="text-sm font-medium text-slate-700 mb-1">Stripe Account</p>
<p className="text-sm text-slate-600 font-mono break-all">
{stripeAccountId || <span className="text-slate-400">Not connected</span>}
</p>
</div>
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg border border-green-200">
<p className="text-sm font-medium text-slate-700 mb-1">Xero Organisation</p>
<p className="text-sm text-slate-600 font-mono break-all">
{xeroTenantId || <span className="text-slate-400">Not connected</span>}
</p>
</div>
</div>
</div>
{saved && ( {/* Account Configuration */}
<p className="text-sm text-green-600"> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
Saved <h2 className="text-lg font-semibold text-slate-900 mb-6">Xero Account Codes</h2>
</p>
)} <div className="space-y-6">
{/* Sales Account Code */}
<div>
<label className="block text-sm font-medium text-slate-900 mb-2">
Sales Account Code
</label>
<input
type="text"
placeholder="e.g., 200"
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={salesAccountCode}
onChange={(e) => setSalesAccountCode(e.target.value)}
/>
<p className="text-sm text-slate-600 mt-2">
The Xero account code used for sales invoice line items. This is typically your revenue/sales account.
</p>
</div>
{/* Stripe Clearing Account Code */}
<div>
<label className="block text-sm font-medium text-slate-900 mb-2">
Stripe Clearing Account Code
</label>
<input
type="text"
placeholder="e.g., 1200"
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={stripeClearingAccountCode}
onChange={(e) => setStripeClearingAccountCode(e.target.value)}
/>
<p className="text-sm text-slate-600 mt-2">
The Xero account code that receives Stripe payments. This is typically a bank or clearing account.
</p>
</div>
</div>
{/* Save Button and Feedback */}
<div className="mt-8 flex items-center gap-4">
<button
onClick={save}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Save Settings
</button>
{saved && (
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 px-4 py-3 rounded-lg border border-green-200">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Settings saved successfully</span>
</div>
)}
{error && (
<div className="flex items-center gap-2 text-sm text-red-700 bg-red-50 px-4 py-3 rounded-lg border border-red-200">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span>{error}</span>
</div>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Info Card */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4"> How it works</h3>
<p className="text-sm text-slate-700 mb-4">
When a Stripe payment is received, we automatically create an invoice in Xero using:
</p>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex gap-2">
<span className="text-blue-600"></span>
<span><strong>Sales Account:</strong> Invoice line items</span>
</li>
<li className="flex gap-2">
<span className="text-blue-600"></span>
<span><strong>Clearing Account:</strong> Payment received</span>
</li>
</ul>
</div>
{/* Help Card */}
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4">🚀 Getting started</h3>
<ol className="space-y-2 text-sm text-slate-700">
<li><strong>1.</strong> Set the account codes above</li>
<li><strong>2.</strong> Make a test payment in Stripe</li>
<li><strong>3.</strong> Check Xero for the invoice</li>
<li><strong>4.</strong> You're ready to go!</li>
</ol>
</div>
{/* Status Card */}
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4"> Status</h3>
<p className="text-sm text-green-700 font-medium mb-2">Connected & Active</p>
<p className="text-sm text-slate-600">
Your automation is ready. New Stripe payments will create invoices in Xero automatically.
</p>
</div>
</div>
</div>
</div>
</main>
</div> </div>
); );
} }

View file

@ -0,0 +1 @@
@import "tailwindcss";

View file

@ -13,8 +13,15 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Stripe to Xero - Automated Invoice Creation",
description: "Generated by create next app", description: "Automatically turn Stripe payments into paid Xero invoices with proper VAT handling and accounting compliance.",
keywords: ["Stripe", "Xero", "invoicing", "automation", "accounting", "VAT"],
authors: [{ name: "Stripe to Xero" }],
openGraph: {
title: "Stripe to Xero - Automated Invoice Creation",
description: "Automatically turn Stripe payments into paid Xero invoices with proper VAT handling.",
type: "website",
},
}; };
export default function RootLayout({ export default function RootLayout({

View file

@ -1,4 +1,3 @@
// app/login/page.tsx
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
@ -34,51 +33,125 @@ export default function LoginPage() {
} }
} }
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !loading && email && step === "email") {
submit();
}
};
return ( return (
<main className="max-w-md mx-auto p-8 space-y-8"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex flex-col">
<Progress step={step} /> {/* Navigation */}
<nav className="border-b border-slate-200 bg-white/50 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center">
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
S2X
</div>
</div>
</nav>
{step === "email" && ( {/* Main Content */}
<> <main className="flex-1 flex items-center justify-center px-6 py-12">
<h1 className="text-xl font-semibold">Log in</h1> <div className="w-full max-w-md">
{/* Progress Bar */}
<Progress step={step} />
<input {/* Form Card */}
type="email" <div className="mt-12 bg-white border border-slate-200 rounded-xl shadow-sm p-8">
placeholder="enter@email.com" {step === "email" && (
className="w-full border rounded p-2" <>
value={email} <h1 className="text-2xl font-bold text-slate-900 mb-2">
onChange={(e) => setEmail(e.target.value)} Welcome back
disabled={loading} </h1>
/> <p className="text-slate-600 mb-6">
Sign in to your Stripe to Xero automation dashboard
</p>
<button <div className="space-y-4">
onClick={submit} <div>
disabled={loading || !email} <label className="block text-sm font-medium text-slate-700 mb-2">
className="w-full bg-black text-white py-2 rounded" Email address
> </label>
{loading ? "Sending…" : "Send login link"} <input
</button> type="email"
placeholder="you@company.com"
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading}
autoFocus
/>
</div>
{error && ( <button
<p className="text-sm text-red-600">{error}</p> onClick={submit}
)} disabled={loading || !email}
</> className="w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
)} >
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
Sending link
</span>
) : (
"Send login link"
)}
</button>
{step === "sent" && ( {error && (
<> <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<h1 className="text-xl font-semibold">Check your email</h1> <p className="text-sm text-red-700">{error}</p>
</div>
)}
</div>
</>
)}
<p className="text-gray-600"> {step === "sent" && (
We sent a login link to <strong>{email}</strong>. <>
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 bg-green-100 rounded-full mb-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">
Check your email
</h1>
<p className="text-slate-600 mb-4">
We've sent a login link to <strong className="text-slate-900">{email}</strong>
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p className="text-sm text-blue-800">
The link expires in 15 minutes. Check your spam folder if you don't see it.
</p>
</div>
<button
onClick={() => {
setStep("email");
setError(null);
}}
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Try another email
</button>
</div>
</>
)}
</div>
{/* Footer */}
<p className="text-center text-sm text-slate-500 mt-8">
Magic link authentication is secure and passwordless
</p> </p>
</div>
<p className="text-gray-500 text-sm"> </main>
The link expires in 15 minutes. </div>
</p>
</>
)}
</main>
); );
} }
@ -86,16 +159,15 @@ function Progress({ step }: { step: Step }) {
const percent = step === "email" ? 50 : 100; const percent = step === "email" ? 50 : 100;
return ( return (
<div> <div className="space-y-2">
<div className="h-2 bg-gray-200 rounded"> <div className="h-1 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-2 bg-black rounded transition-all" className="h-1 bg-gradient-to-r from-blue-600 to-blue-700 rounded-full transition-all duration-500"
style={{ width: `${percent}%` }} style={{ width: `${percent}%` }}
/> />
</div> </div>
<p className="text-xs font-medium text-slate-600 uppercase tracking-wide">
<p className="text-sm text-gray-600 mt-2"> {step === "email" ? "Step 1 of 2 • Enter email" : "Step 2 of 2 • Check inbox"}
{step === "email" ? "Enter email" : "Check inbox"}
</p> </p>
</div> </div>
); );

View file

@ -1,12 +1,3 @@
// app/page.tsx
//
// CORE MVP PAGE
// Purpose:
// 1. Explain the automation
// 2. Point the user to the next action
//
// Everything else lives elsewhere.
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -14,59 +5,284 @@ export default async function Home() {
const cookieStore = await cookies(); const cookieStore = await cookies();
const session = cookieStore.get("session"); const session = cookieStore.get("session");
// ✅ If already logged in, go straight to app
if (session) { if (session) {
redirect("/app"); redirect("/app");
} }
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-10"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
{/* Navigation */}
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
S2X
</div>
<a
href="/login"
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-slate-900 transition"
>
Log in
</a>
</div>
</nav>
{/* -------------------------------------------------- <main className="pt-20">
What this is {/* Hero Section */}
-------------------------------------------------- */} <section className="max-w-5xl mx-auto px-6 py-20 sm:py-32 text-center">
<section> <div className="inline-flex items-center gap-2 mb-6 px-3 py-1 bg-blue-50 rounded-full border border-blue-200">
<h1 className="text-2xl font-semibold"> <span className="w-2 h-2 bg-blue-600 rounded-full"></span>
Stripe Xero automation <span className="text-sm font-medium text-blue-700">Automate your invoicing</span>
</h1> </div>
<p className="mt-3 text-gray-700"> <h1 className="text-5xl sm:text-6xl font-bold tracking-tight text-slate-900 mb-6">
When a Stripe payment succeeds, a Xero invoice is Stripe payments,
automatically created and marked as paid. <br />
</p> <span className="bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
</section> Xero invoices
</span>
{" "}automatically
</h1>
{/* -------------------------------------------------- <p className="text-xl text-slate-600 max-w-2xl mx-auto mb-8 leading-relaxed">
What the user does Stop manually creating invoices. When a Stripe payment succeeds, a Xero invoice is instantly created and marked as paidwith proper VAT handling and accountant-approved accuracy.
-------------------------------------------------- */} </p>
<section>
<h2 className="text-lg font-medium">
How it works
</h2>
<ol className="mt-3 space-y-2 list-decimal list-inside text-gray-700"> <div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<li>Log in with your email</li> <a
<li>Connect Stripe</li> href="/login"
<li>Connect Xero</li> className="px-8 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition transform hover:scale-105"
<li>Invoices handle themselves. You focus on the business.</li> >
</ol> Get started
</section> </a>
<a
href="#how-it-works"
className="px-8 py-3 bg-white border border-slate-300 text-slate-700 font-semibold rounded-lg hover:bg-slate-50 transition"
>
Learn more
</a>
</div>
{/* -------------------------------------------------- {/* Trust badges */}
Next action <div className="flex flex-wrap justify-center gap-8 text-sm text-slate-600">
-------------------------------------------------- */} <div className="flex items-center gap-2">
<section className="pt-4 border-t"> <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<p className="text-gray-700"> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
Start by logging in. </svg>
</p> <span>No manual work</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>VAT compliant</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Audit ready</span>
</div>
</div>
</section>
<a {/* Features Section */}
href="/login" <section id="how-it-works" className="max-w-5xl mx-auto px-6 py-20 border-t border-slate-200">
className="inline-block mt-4 px-6 py-3 bg-black text-white rounded text-sm" <h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
> How it works
Log in </h2>
</a> <p className="text-center text-slate-600 mb-12 max-w-2xl mx-auto">
</section> Three simple connections, then automation takes over
</main> </p>
<div className="grid md:grid-cols-3 gap-6 mb-12">
{/* Step 1 */}
<div className="bg-white border border-slate-200 rounded-xl p-6 hover:shadow-lg transition">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-blue-600">1</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Connect Stripe</h3>
<p className="text-slate-600">
Link your Stripe account to start monitoring payments in real-time.
</p>
</div>
{/* Step 2 */}
<div className="bg-white border border-slate-200 rounded-xl p-6 hover:shadow-lg transition">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-blue-600">2</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Connect Xero</h3>
<p className="text-slate-600">
Authorize access to your Xero account for invoice creation.
</p>
</div>
{/* Step 3 */}
<div className="bg-white border border-slate-200 rounded-xl p-6 hover:shadow-lg transition">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
<span className="text-2xl font-bold text-blue-600">3</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Done</h3>
<p className="text-slate-600">
Relax as payments automatically become invoices in Xero.
</p>
</div>
</div>
{/* Benefits Grid */}
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl p-6 border border-blue-200">
<h4 className="font-semibold text-slate-900 mb-2">🎯 Zero manual work</h4>
<p className="text-slate-700">
No more spreadsheets, journals, or month-end fixing. Invoices appear instantly and correctly.
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-xl p-6 border border-green-200">
<h4 className="font-semibold text-slate-900 mb-2"> Accounting ready</h4>
<p className="text-slate-700">
VAT compliance built-in. Your accountant will love the audit trail.
</p>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl p-6 border border-purple-200">
<h4 className="font-semibold text-slate-900 mb-2"> Instant & reliable</h4>
<p className="text-slate-700">
Invoices created within seconds of payment. Refunds become credit notes automatically.
</p>
</div>
<div className="bg-gradient-to-br from-amber-50 to-amber-100 rounded-xl p-6 border border-amber-200">
<h4 className="font-semibold text-slate-900 mb-2">🔒 Xero is the source</h4>
<p className="text-slate-700">
Your invoices remain immutable in Xero. Stripe is purely payment execution.
</p>
</div>
</div>
</section>
{/* Perfect for section */}
<section className="max-w-5xl mx-auto px-6 py-20 border-t border-slate-200">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-12">
Who this is for
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="font-semibold text-slate-900 mb-4"> Perfect fit:</h3>
<ul className="space-y-2">
<li className="flex gap-3 text-slate-700">
<span className="text-green-500"></span>
<span>UK businesses using Stripe & Xero</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-green-500"></span>
<span>VAT registered</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-green-500"></span>
<span>One-off, annual, or fixed recurring billing</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-green-500"></span>
<span>Invoices are currently manually created</span>
</li>
</ul>
</div>
<div>
<h3 className="font-semibold text-slate-900 mb-4"> Not a fit:</h3>
<ul className="space-y-2">
<li className="flex gap-3 text-slate-700">
<span className="text-slate-400"></span>
<span>Usage-based or metered billing</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-slate-400"></span>
<span>Proration or complex billing logic</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-slate-400"></span>
<span>Already fully automated elsewhere</span>
</li>
<li className="flex gap-3 text-slate-700">
<span className="text-slate-400"></span>
<span>Non-UK businesses</span>
</li>
</ul>
</div>
</div>
</section>
{/* Pricing Section */}
<section className="max-w-5xl mx-auto px-6 py-20 border-t border-slate-200">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Simple pricing
</h2>
<p className="text-center text-slate-600 mb-12">
No hidden fees. No per-transaction costs.
</p>
<div className="max-w-md mx-auto bg-gradient-to-br from-slate-50 to-slate-100 border-2 border-blue-600 rounded-2xl p-8 text-center">
<h3 className="text-4xl font-bold text-slate-900 mb-2">£200</h3>
<p className="text-slate-600 mb-6">per month</p>
<ul className="space-y-3 text-left mb-8">
<li className="flex gap-3 text-slate-700">
<svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Unlimited invoices</span>
</li>
<li className="flex gap-3 text-slate-700">
<svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>No per-transaction fees</span>
</li>
<li className="flex gap-3 text-slate-700">
<svg className="w-5 h-5 text-green-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span>Full VAT handling</span>
</li>
</ul>
<p className="text-sm text-slate-600 mb-6 pb-6 border-b border-slate-300">
Usually saves £1000s per year in manual work and accounting fixes.
</p>
<a
href="/login"
className="w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
>
Start 14-day trial
</a>
</div>
</section>
{/* CTA Section */}
<section className="max-w-5xl mx-auto px-6 py-20 border-t border-slate-200">
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-2xl p-12 text-center text-white">
<h2 className="text-3xl font-bold mb-4">
Ready to automate?
</h2>
<p className="text-lg opacity-90 mb-8 max-w-2xl mx-auto">
Connect Stripe and Xero, then focus on what matters. Invoicing handles itself.
</p>
<a
href="/login"
className="inline-block px-8 py-3 bg-white text-blue-600 font-semibold rounded-lg hover:bg-slate-50 transition"
>
Get started free
</a>
</div>
</section>
{/* Footer */}
<footer className="border-t border-slate-200 py-12 mt-20">
<div className="max-w-5xl mx-auto px-6 text-center text-slate-600 text-sm">
<p>Building the bridge between Stripe payments and Xero invoicing.</p>
</div>
</footer>
</main>
</div>
); );
} }

View file

@ -0,0 +1,12 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}
export default config