Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function Landing() {
<div className="absolute inset-0 bg-gradient-to-t from-text/90 via-text/45 to-text/25" />
</div>
<div className="mx-auto w-full max-w-6xl px-6 pt-32 pb-16 sm:pb-24">
<p className="mb-5 inline-flex rounded-full bg-bg/15 px-3 py-1 text-[13px] text-bg backdrop-blur">Testnet prototype · settled in test USDG</p>
<p className="mb-5 inline-flex rounded-full bg-bg/15 px-3 py-1 text-[13px] text-bg backdrop-blur">Settled in USDG</p>
<h1 className="max-w-3xl font-display text-[44px] leading-[1.02] text-bg sm:text-7xl">
Book every supplier together. <em className="text-[#cfe4da]">Release deposits only when all of them say yes.</em>
</h1>
Expand All @@ -85,6 +85,12 @@ export default function Landing() {
Create a package
</LinkButton>
</div>
<p className="mt-5 text-[15px] text-bg/85">
Already use Setlo?{" "}
<Link href="/signin" className="font-medium text-bg underline underline-offset-4 hover:text-[#cfe4da]">
Sign in
</Link>
</p>
</div>
</section>

Expand Down
58 changes: 58 additions & 0 deletions app/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Link from "next/link";
import { CONTACT, LegalPage } from "@/components/app/legal";

export const metadata = { title: "Privacy policy" };

export default function Privacy() {
return (
<LegalPage
title="Privacy policy"
intro="This explains what Setlo collects, why, and who can see it. Setlo is a prototype on test networks, and it collects only what it needs to run bookings."
sections={[
{
h: "1. What we collect",
p: [
"Your email address, and the payout account (wallet address) created for it when you sign in.",
"Booking details that agencies enter: package and supplier names, client and supplier emails, amounts, dates and shared terms, along with drafts and invitation links.",
"Records of transactions Setlo relays for you and of emails it sends (recipient, type and subject), plus basic usage events such as a package being created or funded.",
"Your IP address, used briefly for rate limiting to prevent abuse.",
"On the demo page, your browser stores demo keys and progress locally. They are not sent to our database.",
],
},
{
h: "2. How we use it",
p: [
"To sign you in, show you the bookings you are part of, relay your signed approvals, send invitations and payout notices by email, prevent abuse, and understand whether the prototype works. We don't sell your data or use it for advertising.",
],
},
{
h: "3. Public blockchain data",
p: [
"Bookings are settled on public blockchains. Amounts, quotes, payout addresses and transfers are visible to anyone and cannot be deleted. Emails and names are kept off-chain.",
],
},
{
h: "4. Who processes it",
p: [
"Privy (sign-in and payout accounts), Supabase (database), Vercel (hosting), QuickNode (blockchain access) and Google Gmail (sending emails). Each processes data only as needed to provide its service.",
],
},
{
h: "5. Retention and your choices",
p: [
"Because this is a test prototype, data may be reset at any time. You can ask us to delete your off-chain data or ask what we hold about you by emailing " + CONTACT + ". Onchain records can't be removed.",
],
},
{
h: "6. Changes",
p: [
"We may update this policy; the date above shows the latest version.",
<>
See also the <Link className="text-brand underline underline-offset-4" href="/terms">terms and conditions</Link>.
</>,
],
},
]}
/>
);
}
11 changes: 8 additions & 3 deletions app/app/signin/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export function SignIn() {
const { ready, authenticated } = usePrivy();
const { sendCode, loginWithCode, state } = useLoginWithEmail();
const router = useRouter();
const next = safeNext(useSearchParams().get("next"));
const params = useSearchParams();
const next = safeNext(params.get("next"));
const signedOut = params.get("signedout") === "1";
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -50,14 +52,17 @@ export function SignIn() {
<Logo />
</Link>
<Card className="w-full max-w-md">
<h1 className="font-display text-3xl">{sent ? "Check your email" : "Sign in"}</h1>
<h1 className="font-display text-3xl">{sent ? "Check your email" : "Sign in or create an account"}</h1>
<p className="mt-2 text-[15px] text-text-muted">
{sent
? `We sent a 6-digit code to ${email}. It may take a minute, and can land in spam.`
: invite
? "Use the email address your invitation was sent to. You'll go straight to it after signing in."
: "We'll email you a sign-in code. No password or crypto wallet needed."}
: "Enter your email and we'll send a code. No password or crypto wallet needed. If you've used Setlo before, you'll get back the same bookings and payout account."}
</p>
{signedOut && !sent && !error && (
<Notice tone="ok" className="mt-5">You&apos;re signed out. Sign in with the same email to get back to your bookings.</Notice>
)}
{error && <Notice tone="bad" className="mt-5">{error}</Notice>}
{!sent ? (
<Form className="mt-6 flex flex-col gap-4" onSubmit={(e) => { e.preventDefault(); void run(() => sendCode({ email: email.trim() })); }}>
Expand Down
64 changes: 64 additions & 0 deletions app/app/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Link from "next/link";
import { CONTACT, LegalPage } from "@/components/app/legal";

export const metadata = { title: "Terms and conditions" };

export default function Terms() {
return (
<LegalPage
title="Terms and conditions"
intro="These terms cover your use of Setlo, a prototype for shared event bookings and conditional settlement. By signing in or using the demo, you agree to them."
sections={[
{
h: "1. A prototype on test networks",
p: [
"Setlo currently runs only on test networks (Arbitrum Sepolia and Robinhood Chain Testnet) with test USDG. Test USDG has no monetary value and cannot be redeemed. Do not use Setlo for real bookings or real money.",
"The service may change, reset, pause or stop at any time, and test data or balances may be lost.",
],
},
{
h: "2. Accounts",
p: [
"You sign in with an email code. Setlo creates a payout account (a wallet address) linked to that email through our sign-in provider, Privy. You are responsible for keeping access to your email.",
"Setlo checks that an invited person controls the invited email. It does not verify anyone's business identity, licences or ability to deliver an event. Clients should only approve suppliers they trust.",
],
},
{
h: "3. How bookings and payments work",
p: [
"Bookings are settled by the SetloPackages smart contract. Its rules decide when deposits are released, when hold fees are earned, and when money is returned. Once a transaction is confirmed onchain, Setlo cannot reverse it.",
"If a required supplier doesn't accept in time, or the package expires or is cancelled, the client gets the remaining funds back minus any hold fees they approved and that were earned. Setlo does not resolve disputes about the quality or delivery of an event.",
"Paxos, the issuer of USDG, can pause the token or freeze addresses, including the Setlo contract. That could delay or block payments.",
],
},
{
h: "4. Public blockchain data",
p: [
"Amounts, quotes, payout addresses and transaction history recorded onchain are public and permanent. Don't put information in a booking that you aren't comfortable being public.",
],
},
{
h: "5. Acceptable use",
p: [
"Don't use Setlo to break the law, impersonate others, send spam through invitations, abuse the test faucets or interfere with the service. We may limit or remove access that does.",
],
},
{
h: "6. No warranty and limited liability",
p: [
"Setlo is provided as is, without warranties of any kind. Smart contracts, wallets, networks and third-party services can fail. To the extent the law allows, Setlo and its builders are not liable for losses arising from your use of the prototype.",
],
},
{
h: "7. Changes and contact",
p: [
"We may update these terms; the date above shows the latest version. Questions: " + CONTACT + ".",
<>
See also the <Link className="text-brand underline underline-offset-4" href="/privacy">privacy policy</Link>.
</>,
],
},
]}
/>
);
}
31 changes: 31 additions & 0 deletions app/components/app/legal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Link from "next/link";
import { Footer, Logo } from "./shell";

export const UPDATED = "3 October 2026";
export const CONTACT = "setlo.app@gmail.com";

export function LegalPage({ title, intro, sections }: { title: string; intro: string; sections: { h: string; p: React.ReactNode[] }[] }) {
return (
<>
<header className="border-b border-border bg-bg">
<div className="mx-auto flex h-16 max-w-6xl items-center px-4 sm:px-6">
<Link href="/" aria-label="Setlo home"><Logo /></Link>
</div>
</header>
<main className="mx-auto max-w-2xl px-6 py-14 sm:py-20">
<h1 className="font-display text-4xl sm:text-5xl">{title}</h1>
<p className="mt-3 text-sm text-text-muted">Last updated {UPDATED}</p>
<p className="mt-6 text-lg text-text-muted">{intro}</p>
{sections.map((s) => (
<section key={s.h} className="mt-10">
<h2 className="text-xl font-semibold">{s.h}</h2>
{s.p.map((x, i) => (
<p key={i} className="mt-3 text-[15px] leading-relaxed text-text-muted">{x}</p>
))}
</section>
))}
</main>
<Footer />
</>
);
}
17 changes: 12 additions & 5 deletions app/components/app/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useCreateWallet, usePrivy, useSignTypedData, useWallets } from "@privy-io/react-auth";
import { usePathname, useRouter } from "next/navigation";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { Address, EIP1193Provider, Hex } from "viem";
import { Button } from "@/components/ui/button";
import { ErrorState } from "@/components/ui/status";
Expand Down Expand Up @@ -42,10 +42,17 @@ export function RequireSession({ children }: { children: React.ReactNode }) {
const [error, setError] = useState<string | null>(null);
const [attempt, setAttempt] = useState(0);

const leaving = useRef(false);
useEffect(() => {
if (ready && !authenticated) router.replace(`/signin?next=${encodeURIComponent(path)}`);
if (ready && !authenticated && !leaving.current) router.replace(`/signin?next=${encodeURIComponent(path)}`);
}, [ready, authenticated, router, path]);

const signOut = useCallback(async () => {
leaving.current = true;
await logout();
router.replace(`/signin?signedout=1&next=${encodeURIComponent(path)}`);
}, [logout, router, path]);

const embedded = wallets.find((w) => w.walletClientType === "privy");
const address = (user?.wallet?.address ?? embedded?.address) as Address | undefined;

Expand Down Expand Up @@ -96,8 +103,8 @@ export function RequireSession({ children }: { children: React.ReactNode }) {
(id) => own.switchChain(id),
async (req) => (await signTypedData(JSON.parse(JSON.stringify(req)), { address: synced })).signature as Hex,
);
return { email: user.email.address.toLowerCase(), wallet: synced, signer, api, logout };
}, [synced, own, user, signTypedData, api, logout]);
return { email: user.email.address.toLowerCase(), wallet: synced, signer, api, logout: signOut };
}, [synced, own, user, signTypedData, api, signOut]);

if (!ready || !authenticated) return <PageLoading label="Checking your sign-in" />;
if (error && !session)
Expand All @@ -108,7 +115,7 @@ export function RequireSession({ children }: { children: React.ReactNode }) {
);
if (walletsReady && synced && !own)
return (
<ErrorState className="px-4 pt-24" title="Payout account not available" action={<Button onPress={() => logout()}>Sign out</Button>}>
<ErrorState className="px-4 pt-24" title="Payout account not available" action={<Button onPress={() => void signOut()}>Sign out</Button>}>
This browser can&apos;t reach the payout account registered to your email ({synced}). Sign out and sign in again.
</ErrorState>
);
Expand Down
12 changes: 8 additions & 4 deletions app/components/app/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function Footer() {
<div className="mx-auto grid max-w-6xl gap-10 px-6 py-14 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col gap-3">
<Logo />
<p className="max-w-xs text-sm text-text-muted">Shared booking and settlement for event agencies, clients and suppliers. Testnet prototype, settled in test USDG.</p>
<p className="max-w-xs text-sm text-text-muted">Shared booking and settlement for event agencies, clients and suppliers. Settled in USDG.</p>
</div>
<nav aria-label="Product" className="flex flex-col gap-2 text-sm">
<p className="font-semibold">Product</p>
Expand Down Expand Up @@ -67,9 +67,13 @@ export function Footer() {
</div>
</div>
<div className="border-t border-border">
<p className="mx-auto max-w-6xl px-6 py-5 text-[13px] text-text-muted">
Test networks only. USDG here is test money with no value. Paxos can pause USDG or freeze addresses, including the Setlo contract.
</p>
<div className="mx-auto flex max-w-6xl flex-col gap-3 px-6 py-5 text-[13px] text-text-muted sm:flex-row sm:items-center sm:justify-between">
<p>Test networks only. USDG here is test money with no value. Paxos can pause USDG or freeze addresses, including the Setlo contract.</p>
<nav aria-label="Legal" className="flex shrink-0 gap-4">
<Link className="hover:text-text" href="/terms">Terms and conditions</Link>
<Link className="hover:text-text" href="/privacy">Privacy policy</Link>
</nav>
</div>
</div>
</footer>
);
Expand Down
Loading