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">
<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>
<main className="pt-20">
<div className="max-w-2xl mx-auto px-6 py-16">
{/* Welcome Section */}
<div className="mb-12">
<h1 className="text-4xl font-bold text-slate-900 mb-3">
Welcome, {user.email?.split("@")[0]}! 👋
</h1> </h1>
<p className="text-lg text-slate-600">
Let's set up your Stripe to Xero automation in just two steps.
</p>
</div>
{/* Progress */} {/* Progress Visualization */}
<ol className="space-y-4"> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<li className="flex items-center gap-3"> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
<span className="text-green-600"></span>
<span>
Logged in as <strong>{user.email}</strong>
</span>
</li>
<li className="flex items-center gap-3"> <div className="space-y-4">
<span className="text-blue-600"></span> {/* Step 1 - Login */}
<span className="font-medium">Connect Stripe</span> <div className="flex gap-4">
</li> <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>
<li className="text-gray-400"> {/* Step 2 - Stripe */}
Xero will be connected after Stripe <div className="flex gap-4">
</li> <div className="flex flex-col items-center">
</ol> <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">
2
</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">Connect Stripe</p>
<p className="text-sm text-slate-600">Link your Stripe account to detect payments</p>
</div>
</div>
{/* Primary CTA */} {/* Step 3 - Xero */}
<div className="pt-6"> <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 <Link
href="/connect/stripe" href="/connect/stripe"
className="inline-block rounded bg-black text-white px-5 py-3" 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 Connect Stripe
</Link> </Link>
</div> </div>
</div>
</main> </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
</div>
</div>
</nav>
<main className="pt-20">
<div className="max-w-2xl mx-auto px-6 py-16">
{/* Header */}
<div className="mb-12">
<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 2 of 3</span>
</div>
<h1 className="text-4xl font-bold text-slate-900 mb-4">
Connect Stripe Connect Stripe
</h1> </h1>
<p className="mt-3 text-gray-700"> <p className="text-lg text-slate-600">
We need read-only access to your Stripe account so we can We need read-only access to your Stripe account to monitor successful payments and automatically create invoices in Xero.
detect successful payments and automatically reconcile
invoices in Xero.
</p> </p>
</section> </div>
{/* -------------------------------------------------- {/* Two Column Layout */}
What will happen <div className="grid md:grid-cols-3 gap-8 mb-12">
-------------------------------------------------- */} {/* Left Column - Information */}
<section> <div className="md:col-span-2">
<h2 className="text-lg font-medium"> {/* What we access */}
What happens next <div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
</h2> <h2 className="text-lg font-semibold text-slate-900 mb-4">What we can access</h2>
<ul className="space-y-3">
<ul className="mt-3 space-y-2 list-disc list-inside text-gray-700"> <li className="flex gap-3">
<li>Youll be redirected to Stripe</li> <svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<li>Youll choose which Stripe account to connect</li> <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" />
<li>Youll be sent back here once connected</li> </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> </ul>
</section> </div>
{/* -------------------------------------------------- {/* What we cannot do */}
Trust / reassurance <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="text-sm text-gray-600"> <ul className="space-y-2 text-sm text-slate-700">
<p> <li className="flex gap-2">
We never see your passwords. <span className="text-amber-600"></span>
<br /> <span>Charge customers or initiate refunds</span>
Access can be revoked at any time from Stripe. </li>
</p> <li className="flex gap-2">
</section> <span className="text-amber-600"></span>
<span>See or store sensitive payment information</span>
</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 */}
Primary action <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>
<section className="pt-4 border-t"> <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 <a
href="/api/stripe/connect" href="/api/stripe/connect"
className="inline-block px-6 py-3 bg-black text-white rounded text-sm" 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 Connect Stripe
</a> </a>
</section> <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> </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">
<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>
<main className="pt-20">
<div className="max-w-2xl mx-auto px-6 py-16">
{/* Success Animation */}
<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>
<h1 className="text-4xl font-bold text-slate-900 mb-3">
Stripe connected! 🎉
</h1> </h1>
<p className="text-gray-600"> <p className="text-lg text-slate-600">
Your Stripe account is now linked. We can now detect successful Your Stripe account is now securely linked. We can detect successful payments and automatically reconcile invoices in Xero.
payments and automatically reconcile invoices in Xero.
</p> </p>
</div>
{/* Progress */} {/* Progress Card */}
<ol className="space-y-4"> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<li className="flex items-center gap-3"> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
<span className="text-green-600"></span>
<span>Logged in</span>
</li>
<li className="flex items-center gap-3"> <div className="space-y-4">
<span className="text-green-600"></span> {/* Step 1 - Login */}
<span>Stripe connected</span> <div className="flex gap-4">
</li> <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">Authentication complete</p>
</div>
</div>
<li className="flex items-center gap-3 text-blue-600"> {/* Step 2 - Stripe */}
<span></span> <div className="flex gap-4">
<span className="font-medium">Connect Xero</span> <div className="flex flex-col items-center">
</li> <div className="w-10 h-10 rounded-full bg-green-100 border-2 border-green-600 flex items-center justify-center flex-shrink-0">
</ol> <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>
{/* Primary CTA */} {/* Step 3 - Xero */}
<div className="pt-6 border-t"> <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 <Link
href="/connect/xero" href="/connect/xero"
className="inline-block rounded bg-black text-white px-5 py-3" 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 Connect Xero Continue to Xero
</Link> </Link>
</div> </div>
</div>
</main> </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
</div>
</div>
</nav>
<main className="pt-20">
<div className="max-w-2xl mx-auto px-6 py-16">
{/* Header */}
<div className="mb-12">
<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">
Connect Xero Connect Xero
</h1> </h1>
<p className="mt-3 text-gray-700"> <p className="text-lg text-slate-600">
We need access to your Xero organisation so we can automatically We need access to your Xero organisation to automatically create invoices and mark them as paid when Stripe payments succeed.
create invoices and mark them as paid when Stripe payments succeed.
</p> </p>
</section> </div>
{/* -------------------------------------------------- {/* Two Column Layout */}
What will happen <div className="grid md:grid-cols-3 gap-8 mb-12">
-------------------------------------------------- */} {/* Left Column - Information */}
<section> <div className="md:col-span-2">
<h2 className="text-lg font-medium"> {/* What we do */}
What happens next <div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
</h2> <h2 className="text-lg font-semibold text-slate-900 mb-4">What we can create</h2>
<ul className="space-y-3">
<ul className="mt-3 space-y-2 list-disc list-inside text-gray-700"> <li className="flex gap-3">
<li>Youll be redirected to Xero</li> <svg className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<li>Youll choose which organisation to connect</li> <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" />
<li>Youll be sent back here once connected</li> </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> </ul>
</section> </div>
{/* -------------------------------------------------- {/* Important notes */}
Trust / reassurance <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="text-sm text-gray-600"> <ul className="space-y-2 text-sm text-slate-700">
<p> <li className="flex gap-2">
We never see your Xero password. <span className="text-purple-600 font-bold"></span>
<br /> <span>We only create invoices; we never modify or delete them</span>
Access can be revoked at any time from Xero. </li>
</p> <li className="flex gap-2">
</section> <span className="text-purple-600 font-bold"></span>
<span>All invoices are created as draft and marked as paid automatically</span>
</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 */}
Primary action <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>
<section className="pt-4 border-t"> <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 <a
href="/api/xero/connect" href="/api/xero/connect"
className="inline-block px-6 py-3 bg-black text-white rounded text-sm" 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 Connect Xero
</a> </a>
</section> <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> </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">
<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>
<main className="pt-20">
<div className="max-w-2xl mx-auto px-6 py-16">
{/* Success Animation */}
<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>
<h1 className="text-4xl font-bold text-slate-900 mb-3">
All set! 🚀
</h1> </h1>
<p className="text-gray-600"> <p className="text-lg text-slate-600">
Your Xero organisation is now linked. We can now automatically Your Xero organisation is now securely linked. We can now automatically create invoices and mark them as paid when Stripe payments succeed.
create invoices and mark them as paid when Stripe payments succeed.
</p> </p>
</div>
{/* Progress */} {/* Progress Card */}
<ol className="space-y-4"> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<li className="flex items-center gap-3"> <h2 className="text-lg font-semibold text-slate-900 mb-6">Setup complete</h2>
<span className="text-green-600"></span>
<span>Logged in</span> <div className="space-y-4">
{/* Step 1 - Login */}
<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">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>
<li className="flex gap-3">
<li className="flex items-center gap-3"> <span className="text-blue-600 font-bold text-lg"></span>
<span className="text-green-600"></span> <span className="text-slate-700">Make a test Stripe payment to verify everything works</span>
<span>Stripe connected</span>
</li> </li>
<li className="flex gap-3">
<li className="flex items-center gap-3"> <span className="text-blue-600 font-bold text-lg"></span>
<span className="text-green-600"></span> <span className="text-slate-700">Watch as invoices automatically appear in Xero</span>
<span>Xero connected</span>
</li> </li>
</ol> </ul>
</div>
{/* Primary CTA */} {/* CTA */}
<div className="pt-6 border-t"> <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 <Link
href="/dashboard" href="/dashboard"
className="inline-block rounded bg-black text-white px-5 py-3" 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"
> >
Go to dashboard Open dashboard
</Link> </Link>
</div> </div>
</div>
</main> </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,7 +26,9 @@ export default function DashboardPage() {
async function save() { async function save() {
setSaved(false); setSaved(false);
setError(null);
try {
await fetch("/api/dashboard/xero-settings", { await fetch("/api/dashboard/xero-settings", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -37,77 +39,182 @@ export default function DashboardPage() {
}); });
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 (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
<div className="text-center">
<div className="inline-flex items-center justify-center w-12 h-12 mb-4">
<span className="w-3 h-3 bg-blue-600 rounded-full animate-spin"></span>
</div>
<p className="text-slate-600">Loading your settings</p>
</div>
</div>
);
}
return ( return (
<div className="max-w-xl space-y-6"> <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<h1 className="text-2xl font-semibold"> {/* Navigation */}
Stripe Xero settings <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">Dashboard</div>
</div>
</nav>
<main className="pt-20">
<div className="max-w-4xl mx-auto px-6 py-16">
{/* Header */}
<div className="mb-12">
<h1 className="text-4xl font-bold text-slate-900 mb-3">Dashboard</h1>
<p className="text-lg text-slate-600">
Configure your Xero account codes and manage your automation settings.
</p>
</div>
<div className="grid lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Connected Accounts */} {/* Connected Accounts */}
<div className="border rounded p-4 bg-gray-50 space-y-3"> <div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
<h2 className="font-medium text-sm text-gray-600">Connected Accounts</h2> <h2 className="text-lg font-semibold text-slate-900 mb-4">Connected Accounts</h2>
<div className="space-y-2 text-sm"> <div className="space-y-4">
<p> <div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg border border-blue-200">
<span className="font-medium">Stripe:</span>{" "} <p className="text-sm font-medium text-slate-700 mb-1">Stripe Account</p>
<code className="bg-white px-2 py-1 rounded text-xs"> <p className="text-sm text-slate-600 font-mono break-all">
{stripeAccountId || "Not connected"} {stripeAccountId || <span className="text-slate-400">Not connected</span>}
</code>
</p> </p>
<p> </div>
<span className="font-medium">Xero:</span>{" "} <div className="p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg border border-green-200">
<code className="bg-white px-2 py-1 rounded text-xs"> <p className="text-sm font-medium text-slate-700 mb-1">Xero Organisation</p>
{xeroTenantId || "Not connected"} <p className="text-sm text-slate-600 font-mono break-all">
</code> {xeroTenantId || <span className="text-slate-400">Not connected</span>}
</p> </p>
</div> </div>
</div> </div>
</div>
{/* Account Configuration */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Xero Account Codes</h2>
<div className="space-y-6">
{/* Sales Account Code */}
<div> <div>
<label className="block text-sm font-medium"> <label className="block text-sm font-medium text-slate-900 mb-2">
Sales account code Sales Account Code
</label> </label>
<input <input
className="mt-1 w-full border px-3 py-2" 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} value={salesAccountCode}
onChange={(e) => setSalesAccountCode(e.target.value)} onChange={(e) => setSalesAccountCode(e.target.value)}
/> />
<p className="text-sm text-zinc-500"> <p className="text-sm text-slate-600 mt-2">
Used on invoice line items The Xero account code used for sales invoice line items. This is typically your revenue/sales account.
</p> </p>
</div> </div>
{/* Stripe Clearing Account Code */}
<div> <div>
<label className="block text-sm font-medium"> <label className="block text-sm font-medium text-slate-900 mb-2">
Stripe clearing account code Stripe Clearing Account Code
</label> </label>
<input <input
className="mt-1 w-full border px-3 py-2" 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} value={stripeClearingAccountCode}
onChange={(e) => onChange={(e) => setStripeClearingAccountCode(e.target.value)}
setStripeClearingAccountCode(e.target.value)
}
/> />
<p className="text-sm text-zinc-500"> <p className="text-sm text-slate-600 mt-2">
Receives Stripe payments The Xero account code that receives Stripe payments. This is typically a bank or clearing account.
</p> </p>
</div> </div>
</div>
{/* Save Button and Feedback */}
<div className="mt-8 flex items-center gap-4">
<button <button
onClick={save} onClick={save}
className="rounded bg-black px-4 py-2 text-white" 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 Save Settings
</button> </button>
{saved && ( {saved && (
<p className="text-sm text-green-600"> <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">
Saved <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
</p> <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">
{/* 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>
{/* Main Content */}
<main className="flex-1 flex items-center justify-center px-6 py-12">
<div className="w-full max-w-md">
{/* Progress Bar */}
<Progress step={step} /> <Progress step={step} />
{/* Form Card */}
<div className="mt-12 bg-white border border-slate-200 rounded-xl shadow-sm p-8">
{step === "email" && ( {step === "email" && (
<> <>
<h1 className="text-xl font-semibold">Log in</h1> <h1 className="text-2xl font-bold text-slate-900 mb-2">
Welcome back
</h1>
<p className="text-slate-600 mb-6">
Sign in to your Stripe to Xero automation dashboard
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Email address
</label>
<input <input
type="email" type="email"
placeholder="enter@email.com" placeholder="you@company.com"
className="w-full border rounded p-2" 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} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading} disabled={loading}
autoFocus
/> />
</div>
<button <button
onClick={submit} onClick={submit}
disabled={loading || !email} disabled={loading || !email}
className="w-full bg-black text-white py-2 rounded" 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 ? "Sending…" : "Send login link"} {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> </button>
{error && ( {error && (
<p className="text-sm text-red-600">{error}</p> <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-700">{error}</p>
</div>
)} )}
</div>
</> </>
)} )}
{step === "sent" && ( {step === "sent" && (
<> <>
<h1 className="text-xl font-semibold">Check your email</h1> <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>
<p className="text-gray-600"> <h1 className="text-2xl font-bold text-slate-900 mb-2">
We sent a login link to <strong>{email}</strong>. 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> </p>
<p className="text-gray-500 text-sm"> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
The link expires in 15 minutes. <p className="text-sm text-blue-800">
The link expires in 15 minutes. Check your spam folder if you don't see it.
</p> </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>
</div>
</main> </main>
</div>
); );
} }
@ -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">
What this is <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">
<section> S2X
<h1 className="text-2xl font-semibold"> </div>
Stripe Xero automation
</h1>
<p className="mt-3 text-gray-700">
When a Stripe payment succeeds, a Xero invoice is
automatically created and marked as paid.
</p>
</section>
{/* --------------------------------------------------
What the user does
-------------------------------------------------- */}
<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">
<li>Log in with your email</li>
<li>Connect Stripe</li>
<li>Connect Xero</li>
<li>Invoices handle themselves. You focus on the business.</li>
</ol>
</section>
{/* --------------------------------------------------
Next action
-------------------------------------------------- */}
<section className="pt-4 border-t">
<p className="text-gray-700">
Start by logging in.
</p>
<a <a
href="/login" href="/login"
className="inline-block mt-4 px-6 py-3 bg-black text-white rounded text-sm" className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-slate-900 transition"
> >
Log in Log in
</a> </a>
</div>
</nav>
<main className="pt-20">
{/* Hero Section */}
<section className="max-w-5xl mx-auto px-6 py-20 sm:py-32 text-center">
<div className="inline-flex items-center gap-2 mb-6 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">Automate your invoicing</span>
</div>
<h1 className="text-5xl sm:text-6xl font-bold tracking-tight text-slate-900 mb-6">
Stripe payments,
<br />
<span className="bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
Xero invoices
</span>
{" "}automatically
</h1>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-8 leading-relaxed">
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>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<a
href="/login"
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"
>
Get started
</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 */}
<div className="flex flex-wrap justify-center gap-8 text-sm text-slate-600">
<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>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> </section>
{/* Features Section */}
<section id="how-it-works" 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">
How it works
</h2>
<p className="text-center text-slate-600 mb-12 max-w-2xl mx-auto">
Three simple connections, then automation takes over
</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> </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