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)
* Stripe OAuth:
### Frontend Improvements Details
**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:
* 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:
* 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.
* [ ] Fix Xero contact creation:
* [x] ~~Fix Xero contact creation~~ ✅ DONE
* Check by email → reuse if exists → only create if missing
* [ ] Fix Stripe OAuth app reuse (stop creating new apps)
* [ ] Ensure default Xero account codes are set **on first connection**
* sales = 200
* clearing = 610
* [ ] Re-enable “mark invoice as paid” via Stripe Clearing once accounts are valid
* ~~Check by email → reuse if exists → only create if missing~~
* [x] ~~Fix Stripe OAuth app reuse (stop creating new apps)~~ ✅ DONE
* [x] ~~Re-enable "mark invoice as paid" via Stripe Clearing once accounts are valid~~ ✅ DONE
> 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)
* One screen before enabling sync:
* [x] ~~Dashboard shows connected accounts~~ ✅ DONE
* ✔ Stripe connected
* ✔ Xero connected
* ✔ VAT status detected
* ✔ Sales account code shown (read-only for now)
* ✔ Stripe clearing account shown
* Even if its ugly — this prevents 80% of future support pain
* ~~Stripe account ID displayed~~
* ~~Xero tenant ID displayed~~
* [x] ~~Smart redirect flow based on connection state~~ ✅ DONE
* [ ] VAT status detection
* [ ] Sales account code shown (editable)
* [ ] 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**:
@ -116,16 +142,16 @@ These unlock charging real money.
* A cold UK Stripe + Xero business with obvious VAT needs
* Offer:
* £10£30/month
* “Early access / founder pricing”
* £15/month Starter plan
* "Early access / founder pricing" (50% off for life)
* Manual support included
* 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
* Target:
@ -135,21 +161,81 @@ These unlock charging real money.
* Clearly VAT-registered
* 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**
* 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
* Auto-detect or create Stripe Clearing account in Xero
* Bulk historical invoice sync
* Invoice preview before creation
* Reduce manual fixes you find yourself repeating
* Nothing else until:
* 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 Link from "next/link";
import { eq } from "drizzle-orm";
@ -14,65 +13,119 @@ export default async function AppPage() {
redirect("/login");
}
// Check if user has Stripe connection
const stripeConn = await db
.select()
.from(stripeAccounts)
.where(eq(stripeAccounts.userId, user.id))
.limit(1);
// Check if user has Xero connection
const xeroConn = await db
.select()
.from(xeroConnections)
.where(eq(xeroConnections.userId, user.id))
.limit(1);
// If both connected, go to dashboard
if (stripeConn.length > 0 && xeroConn.length > 0) {
redirect("/dashboard");
}
// If only Stripe connected, go to Xero connection
if (stripeConn.length > 0 && xeroConn.length === 0) {
redirect("/connect/xero");
}
// Otherwise, show Stripe connection step
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
<h1 className="text-2xl font-semibold">
Welcome{user.email ? `, ${user.email}` : ""}
<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>
<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>
<p className="text-lg text-slate-600">
Let's set up your Stripe to Xero automation in just two steps.
</p>
</div>
{/* Progress */}
<ol className="space-y-4">
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>
Logged in as <strong>{user.email}</strong>
</span>
</li>
{/* Progress Visualization */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
<li className="flex items-center gap-3">
<span className="text-blue-600"></span>
<span className="font-medium">Connect Stripe</span>
</li>
<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">{user.email}</p>
</div>
</div>
<li className="text-gray-400">
Xero will be connected after Stripe
</li>
</ol>
{/* Step 2 - Stripe */}
<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">
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 */}
<div className="pt-6">
{/* 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 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>
</div>
</div>
</main>
</div>
);
}

View file

@ -33,8 +33,35 @@ export default function AuthCallbackClient() {
}, [params, router]);
return (
<main className="min-h-screen flex items-center justify-center">
<p className="text-sm text-gray-500">Signing you in</p>
</main>
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
<div className="text-center">
{/* 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 { redirect } from "next/navigation";
@ -13,65 +5,132 @@ export default async function ConnectStripePage() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
// Safety: if not logged in, bounce to login
if (!session) {
redirect("/login");
}
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
{/* --------------------------------------------------
Header
-------------------------------------------------- */}
<section>
<h1 className="text-2xl font-semibold">
<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">
<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">
{/* 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
</h1>
<p className="mt-3 text-gray-700">
We need read-only access to your Stripe account so we can
detect successful payments and automatically reconcile
invoices in Xero.
<p className="text-lg text-slate-600">
We need read-only access to your Stripe account to monitor successful payments and automatically create invoices in Xero.
</p>
</section>
</div>
{/* --------------------------------------------------
What will happen
-------------------------------------------------- */}
<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">
<li>Youll be redirected to Stripe</li>
<li>Youll choose which Stripe account to connect</li>
<li>Youll be sent back here once connected</li>
{/* Two Column Layout */}
<div className="grid md:grid-cols-3 gap-8 mb-12">
{/* Left Column - Information */}
<div className="md:col-span-2">
{/* What we access */}
<div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">What we can access</h2>
<ul className="space-y-3">
<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">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>
</section>
</div>
{/* --------------------------------------------------
Trust / reassurance
-------------------------------------------------- */}
<section className="text-sm text-gray-600">
<p>
We never see your passwords.
<br />
Access can be revoked at any time from Stripe.
</p>
</section>
{/* What we cannot do */}
<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>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex gap-2">
<span className="text-amber-600"></span>
<span>Charge customers or initiate refunds</span>
</li>
<li className="flex gap-2">
<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>
{/* --------------------------------------------------
Primary action
-------------------------------------------------- */}
<section className="pt-4 border-t">
{/* 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="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
</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>
</div>
);
}

View file

@ -2,43 +2,104 @@ import Link from "next/link";
export default function StripeSuccessPage() {
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
<h1 className="text-2xl font-semibold">
Stripe connected 🎉
<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">
<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>
<p className="text-gray-600">
Your Stripe account is now linked. We can now detect successful
payments and automatically reconcile invoices in Xero.
<p className="text-lg text-slate-600">
Your Stripe account is now securely linked. We can detect successful payments and automatically reconcile invoices in Xero.
</p>
</div>
{/* Progress */}
<ol className="space-y-4">
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Logged in</span>
</li>
{/* Progress Card */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Setup progress</h2>
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Stripe connected</span>
</li>
<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>
<li className="flex items-center gap-3 text-blue-600">
<span></span>
<span className="font-medium">Connect Xero</span>
</li>
</ol>
{/* 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>
{/* Primary CTA */}
<div className="pt-6 border-t">
{/* 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 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>
</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 { redirect } from "next/navigation";
@ -11,64 +5,141 @@ export default async function ConnectXeroPage() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
// Safety: if not logged in, bounce to login
if (!session) {
redirect("/login");
}
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
{/* --------------------------------------------------
Header
-------------------------------------------------- */}
<section>
<h1 className="text-2xl font-semibold">
<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">
<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">
{/* 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
</h1>
<p className="mt-3 text-gray-700">
We need access to your Xero organisation so we can automatically
create invoices and mark them as paid when Stripe payments succeed.
<p className="text-lg text-slate-600">
We need access to your Xero organisation to automatically create invoices and mark them as paid when Stripe payments succeed.
</p>
</section>
</div>
{/* --------------------------------------------------
What will happen
-------------------------------------------------- */}
<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">
<li>Youll be redirected to Xero</li>
<li>Youll choose which organisation to connect</li>
<li>Youll be sent back here once connected</li>
{/* Two Column Layout */}
<div className="grid md:grid-cols-3 gap-8 mb-12">
{/* Left Column - Information */}
<div className="md:col-span-2">
{/* What we do */}
<div className="bg-white border border-slate-200 rounded-xl p-6 mb-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">What we can create</h2>
<ul className="space-y-3">
<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">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>
</section>
</div>
{/* --------------------------------------------------
Trust / reassurance
-------------------------------------------------- */}
<section className="text-sm text-gray-600">
<p>
We never see your Xero password.
<br />
Access can be revoked at any time from Xero.
</p>
</section>
{/* Important notes */}
<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>
<ul className="space-y-2 text-sm text-slate-700">
<li className="flex gap-2">
<span className="text-purple-600 font-bold"></span>
<span>We only create invoices; we never modify or delete them</span>
</li>
<li className="flex gap-2">
<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>
{/* --------------------------------------------------
Primary action
-------------------------------------------------- */}
<section className="pt-4 border-t">
{/* 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="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
</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>
</div>
);
}

View file

@ -2,43 +2,125 @@ import Link from "next/link";
export default function XeroSuccessPage() {
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
<h1 className="text-2xl font-semibold">
Xero connected 🎉
<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">
<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>
<p className="text-gray-600">
Your Xero organisation is now linked. We can now automatically
create invoices and mark them as paid when Stripe payments succeed.
<p className="text-lg text-slate-600">
Your Xero organisation is now securely linked. We can now automatically create invoices and mark them as paid when Stripe payments succeed.
</p>
</div>
{/* Progress */}
<ol className="space-y-4">
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Logged in</span>
{/* Progress Card */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-8 mb-8">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Setup complete</h2>
<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 className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Stripe connected</span>
<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 items-center gap-3">
<span className="text-green-600"></span>
<span>Xero connected</span>
<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>
</ol>
</ul>
</div>
{/* Primary CTA */}
<div className="pt-6 border-t">
{/* 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 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>
</div>
</div>
</main>
</div>
);
}

View file

@ -4,12 +4,12 @@ import { useEffect, useState } from "react";
export default function DashboardPage() {
const [salesAccountCode, setSalesAccountCode] = useState("");
const [stripeClearingAccountCode, setStripeClearingAccountCode] =
useState("");
const [stripeClearingAccountCode, setStripeClearingAccountCode] = useState("");
const [stripeAccountId, setStripeAccountId] = useState("");
const [xeroTenantId, setXeroTenantId] = useState("");
const [loading, setLoading] = useState(true);
const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
Promise.all([
@ -26,7 +26,9 @@ export default function DashboardPage() {
async function save() {
setSaved(false);
setError(null);
try {
await fetch("/api/dashboard/xero-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -37,77 +39,182 @@ export default function DashboardPage() {
});
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="max-w-xl space-y-6">
<h1 className="text-2xl font-semibold">
Stripe Xero settings
</h1>
{/* 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 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>
<label className="block text-sm font-medium">
Sales account code
</label>
<input
className="mt-1 w-full border px-3 py-2"
value={salesAccountCode}
onChange={(e) => setSalesAccountCode(e.target.value)}
/>
<p className="text-sm text-zinc-500">
Used on invoice line items
</p>
</div>
<div>
<label className="block text-sm font-medium">
Stripe clearing account code
</label>
<input
className="mt-1 w-full border px-3 py-2"
value={stripeClearingAccountCode}
onChange={(e) =>
setStripeClearingAccountCode(e.target.value)
}
/>
<p className="text-sm text-zinc-500">
Receives Stripe payments
</p>
</div>
<button
onClick={save}
className="rounded bg-black px-4 py-2 text-white"
>
Save
</button>
{saved && (
<p className="text-sm text-green-600">
Saved
</p>
)}
</div>
);
}
return (
<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>
<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 */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
<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>
{/* 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>
<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>
);
}

View file

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

View file

@ -13,8 +13,15 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Stripe to Xero - Automated Invoice Creation",
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({

View file

@ -1,4 +1,3 @@
// app/login/page.tsx
"use client";
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 (
<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} />
{/* Form Card */}
<div className="mt-12 bg-white border border-slate-200 rounded-xl shadow-sm p-8">
{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
type="email"
placeholder="enter@email.com"
className="w-full border rounded p-2"
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>
<button
onClick={submit}
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>
{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" && (
<>
<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">
We sent a login link to <strong>{email}</strong>.
<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>
<p className="text-gray-500 text-sm">
The link expires in 15 minutes.
<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>
</div>
</main>
</div>
);
}
@ -86,16 +159,15 @@ function Progress({ step }: { step: Step }) {
const percent = step === "email" ? 50 : 100;
return (
<div>
<div className="h-2 bg-gray-200 rounded">
<div className="space-y-2">
<div className="h-1 bg-slate-200 rounded-full overflow-hidden">
<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}%` }}
/>
</div>
<p className="text-sm text-gray-600 mt-2">
{step === "email" ? "Enter email" : "Check inbox"}
<p className="text-xs font-medium text-slate-600 uppercase tracking-wide">
{step === "email" ? "Step 1 of 2 • Enter email" : "Step 2 of 2 • Check inbox"}
</p>
</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 { redirect } from "next/navigation";
@ -14,59 +5,284 @@ export default async function Home() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
// ✅ If already logged in, go straight to app
if (session) {
redirect("/app");
}
return (
<main className="max-w-2xl mx-auto p-8 space-y-10">
{/* --------------------------------------------------
What this is
-------------------------------------------------- */}
<section>
<h1 className="text-2xl font-semibold">
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>
<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="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>
</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>
{/* 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>
</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