@pylonsync/create-pylon 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/templates/_root/gitignore +1 -0
- package/templates/backend/mobile/apps/api/README.md +21 -1
- package/templates/backend/mobile/apps/api/app/(site)/layout.tsx +103 -0
- package/templates/backend/mobile/apps/api/app/(site)/not-found.tsx +23 -0
- package/templates/backend/mobile/apps/api/app/(site)/page.tsx +221 -0
- package/templates/backend/mobile/apps/api/app/(site)/privacy/page.tsx +131 -0
- package/templates/backend/mobile/apps/api/app/(site)/support/page.tsx +78 -0
- package/templates/backend/mobile/apps/api/app/(site)/terms/page.tsx +144 -0
- package/templates/backend/mobile/apps/api/app/error.tsx +27 -0
- package/templates/backend/mobile/apps/api/app/globals.css +87 -0
- package/templates/backend/mobile/apps/api/app/layout.tsx +27 -0
- package/templates/backend/mobile/apps/api/app/robots.ts +13 -0
- package/templates/backend/mobile/apps/api/app/sitemap.ts +16 -0
- package/templates/backend/mobile/apps/api/app.ts +11 -3
- package/templates/backend/mobile/apps/api/components/legal.tsx +64 -0
- package/templates/backend/mobile/apps/api/functions/createNote.ts +5 -1
- package/templates/backend/mobile/apps/api/functions/deleteMyData.ts +38 -0
- package/templates/backend/mobile/apps/api/lib/purchases.ts +8 -0
- package/templates/backend/mobile/apps/api/lib/site.ts +94 -0
- package/templates/backend/mobile/apps/api/package.json +8 -1
- package/templates/backend/mobile/apps/api/tsconfig.json +6 -2
- package/templates/expo/mobile/apps/expo/.env.example +13 -4
- package/templates/expo/mobile/apps/expo/README.md +35 -2
- package/templates/expo/mobile/apps/expo/STORE.md +31 -7
- package/templates/expo/mobile/apps/expo/app/(auth)/sign-in.tsx +17 -6
- package/templates/expo/mobile/apps/expo/app/(auth)/verify.tsx +3 -1
- package/templates/expo/mobile/apps/expo/app/(tabs)/settings.tsx +15 -7
- package/templates/expo/mobile/apps/expo/app/paywall.tsx +9 -13
- package/templates/expo/mobile/apps/expo/app.config.ts +29 -5
- package/templates/expo/mobile/apps/expo/babel.config.js +2 -2
- package/templates/expo/mobile/apps/expo/eas.json +6 -5
- package/templates/expo/mobile/apps/expo/package.json +9 -5
- package/templates/expo/mobile/apps/expo/src/entitlements.ts +4 -9
- package/templates/expo/mobile/apps/expo/src/links.ts +29 -0
- package/templates/expo/mobile/apps/expo/src/pylon.ts +7 -2
- package/templates/expo/mobile/apps/expo/src/session.tsx +69 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"create-pylon": "./bin/create-pylon.js"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node --test"
|
|
13
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin",
|
|
@@ -6,10 +6,28 @@ data API, realtime sync, auth, and the RevenueCat webhook.
|
|
|
6
6
|
```
|
|
7
7
|
app.ts User + Note + RcEntitlement (from @pylonsync/revenuecat)
|
|
8
8
|
functions/createNote server-enforced free-tier cap, then insert
|
|
9
|
+
functions/deleteMyData the user's rows, run by DELETE /api/auth/account
|
|
9
10
|
functions/revenuecatWebhook, syncEntitlements, _pylonRcUpsertEntitlement
|
|
10
11
|
lib/purchases.ts the RevenueCat plugin instance + FREE_NOTE_LIMIT
|
|
12
|
+
lib/site.ts everything the website says — edit this one file
|
|
13
|
+
app/(site)/ landing page, /support, /privacy, /terms
|
|
11
14
|
```
|
|
12
15
|
|
|
16
|
+
## The website
|
|
17
|
+
|
|
18
|
+
This one server answers the app's API calls and serves the public site, so
|
|
19
|
+
`/privacy` and `/terms` are live on the same host the moment you deploy. Both
|
|
20
|
+
stores refuse a submission without a reachable privacy policy URL, and the app
|
|
21
|
+
links to these pages from Settings and from the paywall.
|
|
22
|
+
|
|
23
|
+
Edit `lib/site.ts` for the name, the copy, store links, and your company
|
|
24
|
+
details. A banner sits on every page until those details are filled in,
|
|
25
|
+
because the legal text ships as a draft: it describes what this app actually
|
|
26
|
+
does, which makes it a real starting point, but a lawyer should read it before
|
|
27
|
+
you submit.
|
|
28
|
+
|
|
29
|
+
`bun run dev` serves the site at http://localhost:4321 alongside the API.
|
|
30
|
+
|
|
13
31
|
## Run
|
|
14
32
|
|
|
15
33
|
```bash
|
|
@@ -28,4 +46,6 @@ pylon secrets set REVENUECAT_WEBHOOK_AUTH=... REVENUECAT_SECRET_KEY=... \
|
|
|
28
46
|
```
|
|
29
47
|
|
|
30
48
|
Then set `EXPO_PUBLIC_PYLON_BASE_URL` in `apps/expo/.env` to the deployed URL
|
|
31
|
-
and point the RevenueCat webhook at `<url>/api/fn/revenuecatWebhook`.
|
|
49
|
+
and point the RevenueCat webhook at `<url>/api/fn/revenuecatWebhook`. Put the
|
|
50
|
+
same URL in `lib/site.ts` as `url`, and give App Store Connect `<url>/privacy`
|
|
51
|
+
and `<url>/support`.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "@pylonsync/react";
|
|
3
|
+
import { missingSiteConfig, site } from "@/lib/site";
|
|
4
|
+
|
|
5
|
+
interface LayoutProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `(site)` is a route group: the parens segment never appears in a URL, so
|
|
11
|
+
* `(site)/page.tsx` still serves `/`. This layout owns the chrome for every
|
|
12
|
+
* public page. Keep it here rather than checking the pathname in a component,
|
|
13
|
+
* so a page that should not have the header simply lives outside the group.
|
|
14
|
+
*/
|
|
15
|
+
export default function SiteLayout({ children }: LayoutProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex min-h-screen flex-col">
|
|
18
|
+
<SetupNotice />
|
|
19
|
+
<Header />
|
|
20
|
+
<main className="flex-1">{children}</main>
|
|
21
|
+
<Footer />
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders while `lib/site.ts` still has blank fields. The legal pages below
|
|
28
|
+
* are a starting point with your company details filled in by that file, so
|
|
29
|
+
* an unfinished site should say so out loud rather than publish placeholder
|
|
30
|
+
* terms. It disappears on its own once the fields are set.
|
|
31
|
+
*/
|
|
32
|
+
function SetupNotice() {
|
|
33
|
+
const missing = missingSiteConfig();
|
|
34
|
+
if (missing.length === 0) return null;
|
|
35
|
+
return (
|
|
36
|
+
<div className="border-b border-amber-300 bg-amber-50 px-4 py-3 text-center text-[13px] text-amber-900">
|
|
37
|
+
Finish <code className="font-mono">lib/site.ts</code> before you publish
|
|
38
|
+
this site. Still blank: {missing.join(", ")}. The privacy policy and
|
|
39
|
+
terms need your details, and a lawyer should read them.
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Header() {
|
|
45
|
+
return (
|
|
46
|
+
<header className="sticky top-0 z-20 border-b border-line bg-surface/85 backdrop-blur">
|
|
47
|
+
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-6">
|
|
48
|
+
<Link href="/" className="text-[15px] font-semibold tracking-tight">
|
|
49
|
+
{site.name}
|
|
50
|
+
</Link>
|
|
51
|
+
<nav className="flex items-center gap-6 text-[13.5px] text-ink-muted">
|
|
52
|
+
<a href="/#features" className="hidden transition-colors hover:text-ink sm:block">
|
|
53
|
+
Features
|
|
54
|
+
</a>
|
|
55
|
+
<a href="/#faq" className="hidden transition-colors hover:text-ink sm:block">
|
|
56
|
+
Questions
|
|
57
|
+
</a>
|
|
58
|
+
<Link href="/support" className="transition-colors hover:text-ink">
|
|
59
|
+
Support
|
|
60
|
+
</Link>
|
|
61
|
+
<a
|
|
62
|
+
href="/#get"
|
|
63
|
+
className="rounded-full bg-brand px-4 py-2 text-[13px] font-medium text-brand-ink transition-opacity hover:opacity-90"
|
|
64
|
+
>
|
|
65
|
+
Get the app
|
|
66
|
+
</a>
|
|
67
|
+
</nav>
|
|
68
|
+
</div>
|
|
69
|
+
</header>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Footer() {
|
|
74
|
+
const year = new Date().getFullYear();
|
|
75
|
+
return (
|
|
76
|
+
<footer className="border-t border-line bg-surface-soft">
|
|
77
|
+
<div className="mx-auto flex max-w-5xl flex-col gap-4 px-6 py-10 text-[13px] text-ink-muted sm:flex-row sm:items-center sm:justify-between">
|
|
78
|
+
<p>
|
|
79
|
+
© {year} {site.company || site.name}
|
|
80
|
+
</p>
|
|
81
|
+
<nav className="flex flex-wrap items-center gap-5">
|
|
82
|
+
<Link href="/privacy" className="transition-colors hover:text-ink">
|
|
83
|
+
Privacy
|
|
84
|
+
</Link>
|
|
85
|
+
<Link href="/terms" className="transition-colors hover:text-ink">
|
|
86
|
+
Terms
|
|
87
|
+
</Link>
|
|
88
|
+
<Link href="/support" className="transition-colors hover:text-ink">
|
|
89
|
+
Support
|
|
90
|
+
</Link>
|
|
91
|
+
{site.supportEmail ? (
|
|
92
|
+
<a
|
|
93
|
+
href={`mailto:${site.supportEmail}`}
|
|
94
|
+
className="transition-colors hover:text-ink"
|
|
95
|
+
>
|
|
96
|
+
{site.supportEmail}
|
|
97
|
+
</a>
|
|
98
|
+
) : null}
|
|
99
|
+
</nav>
|
|
100
|
+
</div>
|
|
101
|
+
</footer>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
|
|
4
|
+
// `not-found.tsx` inside the route group, so a missing page still renders
|
|
5
|
+
// with the site header and footer around it.
|
|
6
|
+
export default function NotFound(_props: NotFoundProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
9
|
+
<h1 className="text-3xl font-semibold tracking-[-0.02em]">
|
|
10
|
+
Page not found
|
|
11
|
+
</h1>
|
|
12
|
+
<p className="mt-3 text-[15px] text-ink-muted">
|
|
13
|
+
That link does not go anywhere.
|
|
14
|
+
</p>
|
|
15
|
+
<Link
|
|
16
|
+
href="/"
|
|
17
|
+
className="mt-8 inline-block rounded-xl bg-ink px-5 py-3 text-[14px] font-medium text-surface transition-opacity hover:opacity-90"
|
|
18
|
+
>
|
|
19
|
+
Back to the home page
|
|
20
|
+
</Link>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { site } from "@/lib/site";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `${site.name} — ${site.tagline}`,
|
|
7
|
+
description: site.description,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The landing page, served at `/` by the same Pylon server that answers the
|
|
12
|
+
* mobile app's API calls. That is why the legal pages below are a real URL
|
|
13
|
+
* the moment you deploy: the App Store and Play Store both refuse a
|
|
14
|
+
* submission without one.
|
|
15
|
+
*
|
|
16
|
+
* Replace the copy through `lib/site.ts`. The sections themselves are here.
|
|
17
|
+
*/
|
|
18
|
+
export default function LandingPage() {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<Hero />
|
|
22
|
+
<Features />
|
|
23
|
+
<Pricing />
|
|
24
|
+
<Faq />
|
|
25
|
+
<GetTheApp />
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Hero() {
|
|
31
|
+
return (
|
|
32
|
+
<section className="mx-auto max-w-5xl px-6 pb-16 pt-20 sm:pt-28">
|
|
33
|
+
<div className="grid items-center gap-14 lg:grid-cols-[1.05fr_0.95fr]">
|
|
34
|
+
<div>
|
|
35
|
+
<h1 className="text-4xl font-semibold leading-[1.08] tracking-[-0.02em] sm:text-5xl">
|
|
36
|
+
{site.tagline}
|
|
37
|
+
</h1>
|
|
38
|
+
<p className="mt-5 max-w-xl text-[16.5px] leading-relaxed text-ink-muted">
|
|
39
|
+
{site.description}
|
|
40
|
+
</p>
|
|
41
|
+
<div className="mt-8">
|
|
42
|
+
<StoreButtons />
|
|
43
|
+
</div>
|
|
44
|
+
<p className="mt-4 text-[13px] text-ink-muted">
|
|
45
|
+
Free for your first {site.pricing.freeLimit} notes. No account
|
|
46
|
+
needed to start.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<PhoneShot />
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A real link once the store URL is in `lib/site.ts`, and an honest "not yet"
|
|
57
|
+
* until then. A dead link on a launch page is worse than saying nothing.
|
|
58
|
+
*/
|
|
59
|
+
function StoreButtons() {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-wrap gap-3">
|
|
62
|
+
<StoreButton
|
|
63
|
+
href={site.appStoreUrl}
|
|
64
|
+
label="Download on the App Store"
|
|
65
|
+
pending="Coming to the App Store"
|
|
66
|
+
/>
|
|
67
|
+
<StoreButton
|
|
68
|
+
href={site.playStoreUrl}
|
|
69
|
+
label="Get it on Google Play"
|
|
70
|
+
pending="Coming to Google Play"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function StoreButton({
|
|
77
|
+
href,
|
|
78
|
+
label,
|
|
79
|
+
pending,
|
|
80
|
+
}: {
|
|
81
|
+
href: string;
|
|
82
|
+
label: string;
|
|
83
|
+
pending: string;
|
|
84
|
+
}) {
|
|
85
|
+
if (!href) {
|
|
86
|
+
return (
|
|
87
|
+
<span className="rounded-xl border border-dashed border-line px-5 py-3 text-[14px] text-ink-muted">
|
|
88
|
+
{pending}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return (
|
|
93
|
+
<a
|
|
94
|
+
href={href}
|
|
95
|
+
className="rounded-xl bg-ink px-5 py-3 text-[14px] font-medium text-surface transition-opacity hover:opacity-90"
|
|
96
|
+
>
|
|
97
|
+
{label}
|
|
98
|
+
</a>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Placeholder, marked as one. Swap it for a real screenshot before launch. */
|
|
103
|
+
function PhoneShot() {
|
|
104
|
+
return (
|
|
105
|
+
<div className="mx-auto w-full max-w-[280px]">
|
|
106
|
+
<div className="rounded-[2.25rem] border border-line bg-surface-soft p-3 shadow-[0_30px_80px_-40px_rgba(0,0,0,0.45)]">
|
|
107
|
+
<div className="flex aspect-[9/19.5] flex-col items-center justify-center gap-3 rounded-[1.75rem] border-2 border-dashed border-line px-6 text-center">
|
|
108
|
+
<svg
|
|
109
|
+
width="26"
|
|
110
|
+
height="26"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
fill="none"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth="1.5"
|
|
115
|
+
className="text-ink-muted"
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
>
|
|
118
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
119
|
+
<circle cx="8.5" cy="10" r="1.5" />
|
|
120
|
+
<path d="m21 16-5-5-6 6-3-3-4 4" />
|
|
121
|
+
</svg>
|
|
122
|
+
<p className="text-[13px] font-medium">App screenshot</p>
|
|
123
|
+
<p className="text-[12px] leading-snug text-ink-muted">
|
|
124
|
+
Drop a 1290×2796 screenshot in here. The component is
|
|
125
|
+
<code className="mx-1 font-mono">PhoneShot</code>
|
|
126
|
+
in this file.
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Features() {
|
|
135
|
+
return (
|
|
136
|
+
<section id="features" className="border-t border-line bg-surface-soft">
|
|
137
|
+
<div className="mx-auto max-w-5xl px-6 py-20">
|
|
138
|
+
<h2 className="max-w-2xl text-2xl font-semibold tracking-[-0.015em] sm:text-3xl">
|
|
139
|
+
What it does
|
|
140
|
+
</h2>
|
|
141
|
+
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
142
|
+
{site.features.map((feature) => (
|
|
143
|
+
<div
|
|
144
|
+
key={feature.title}
|
|
145
|
+
className="rounded-2xl border border-line bg-surface p-6"
|
|
146
|
+
>
|
|
147
|
+
<h3 className="text-[15px] font-semibold">{feature.title}</h3>
|
|
148
|
+
<p className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
149
|
+
{feature.body}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Pricing() {
|
|
160
|
+
return (
|
|
161
|
+
<section className="mx-auto max-w-5xl px-6 py-20">
|
|
162
|
+
<h2 className="text-2xl font-semibold tracking-[-0.015em] sm:text-3xl">
|
|
163
|
+
What it costs
|
|
164
|
+
</h2>
|
|
165
|
+
<div className="mt-10 grid gap-6 sm:grid-cols-2">
|
|
166
|
+
<div className="rounded-2xl border border-line p-7">
|
|
167
|
+
<h3 className="text-[15px] font-semibold">Free</h3>
|
|
168
|
+
<p className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
169
|
+
Up to {site.pricing.freeLimit} notes, synced across your devices.
|
|
170
|
+
Nothing expires.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="rounded-2xl border border-brand bg-brand-soft p-7">
|
|
174
|
+
<h3 className="text-[15px] font-semibold">Pro</h3>
|
|
175
|
+
<p className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
176
|
+
{site.pricing.proBlurb} Prices are set per country in the store and
|
|
177
|
+
shown in the app before you buy.
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</section>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function Faq() {
|
|
186
|
+
return (
|
|
187
|
+
<section id="faq" className="border-t border-line bg-surface-soft">
|
|
188
|
+
<div className="mx-auto max-w-3xl px-6 py-20">
|
|
189
|
+
<h2 className="text-2xl font-semibold tracking-[-0.015em] sm:text-3xl">
|
|
190
|
+
Questions
|
|
191
|
+
</h2>
|
|
192
|
+
<dl className="mt-10 divide-y divide-line border-y border-line">
|
|
193
|
+
{site.faq.map((item) => (
|
|
194
|
+
<div key={item.q} className="py-5">
|
|
195
|
+
<dt className="text-[15px] font-medium">{item.q}</dt>
|
|
196
|
+
<dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
197
|
+
{item.a}
|
|
198
|
+
</dd>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
</dl>
|
|
202
|
+
</div>
|
|
203
|
+
</section>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function GetTheApp() {
|
|
208
|
+
return (
|
|
209
|
+
<section id="get" className="mx-auto max-w-5xl px-6 py-20 text-center">
|
|
210
|
+
<h2 className="text-2xl font-semibold tracking-[-0.015em] sm:text-3xl">
|
|
211
|
+
Get {site.name}
|
|
212
|
+
</h2>
|
|
213
|
+
<p className="mx-auto mt-3 max-w-md text-[15px] leading-relaxed text-ink-muted">
|
|
214
|
+
Free to start, on iPhone and Android.
|
|
215
|
+
</p>
|
|
216
|
+
<div className="mt-8 flex justify-center">
|
|
217
|
+
<StoreButtons />
|
|
218
|
+
</div>
|
|
219
|
+
</section>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { Company, LegalPage } from "@/components/legal";
|
|
4
|
+
import { site } from "@/lib/site";
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: `Privacy Policy — ${site.name}`,
|
|
8
|
+
description: `How ${site.name} handles your data.`,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Both stores require a reachable privacy policy URL before they will accept
|
|
13
|
+
* a build, and Apple checks that it matches the App Privacy answers you give
|
|
14
|
+
* in App Store Connect. This text describes what the template's app really
|
|
15
|
+
* does: accounts, notes, and store subscriptions, with no advertising or
|
|
16
|
+
* tracking. Change it as your app changes.
|
|
17
|
+
*/
|
|
18
|
+
export default function PrivacyPage() {
|
|
19
|
+
return (
|
|
20
|
+
<LegalPage title="Privacy Policy">
|
|
21
|
+
<p>
|
|
22
|
+
{site.name} is published by <Company />. This page explains what we
|
|
23
|
+
collect, why we have it, and how to get rid of it.
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<h2>What we collect</h2>
|
|
27
|
+
<ul>
|
|
28
|
+
<li>
|
|
29
|
+
<strong>Your account.</strong> An email address when you sign in with
|
|
30
|
+
a code, or the account identifier and email that Apple or Google
|
|
31
|
+
gives us when you use their sign-in. Apple lets you hide your real
|
|
32
|
+
address, and that works here.
|
|
33
|
+
</li>
|
|
34
|
+
<li>
|
|
35
|
+
<strong>What you write.</strong> The notes you create, so we can
|
|
36
|
+
store them and sync them to your other devices.
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<strong>Your subscription status.</strong> Whether a subscription is
|
|
40
|
+
active, and which product it is. Apple and Google take the payment.
|
|
41
|
+
We never receive your card details.
|
|
42
|
+
</li>
|
|
43
|
+
<li>
|
|
44
|
+
<strong>Technical records.</strong> Ordinary server logs, including
|
|
45
|
+
the internet address your device connects from and the time of the
|
|
46
|
+
request. We use them to keep the service running and to spot abuse.
|
|
47
|
+
</li>
|
|
48
|
+
</ul>
|
|
49
|
+
<p>
|
|
50
|
+
You can use the app without an account. In that case the app still
|
|
51
|
+
creates an anonymous identifier so your notes can sync to the server
|
|
52
|
+
and back. Sign in later and that content moves to your account.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<h2>What we do not do</h2>
|
|
56
|
+
<ul>
|
|
57
|
+
<li>We do not sell your data.</li>
|
|
58
|
+
<li>
|
|
59
|
+
We do not use it for advertising, and we do not track you across
|
|
60
|
+
other apps or websites.
|
|
61
|
+
</li>
|
|
62
|
+
<li>We do not read your notes, except when you ask us to help.</li>
|
|
63
|
+
</ul>
|
|
64
|
+
|
|
65
|
+
<h2>Who else touches it</h2>
|
|
66
|
+
<p>
|
|
67
|
+
We use a small number of companies to run the service, and they may
|
|
68
|
+
process data on our behalf:
|
|
69
|
+
</p>
|
|
70
|
+
<ul>
|
|
71
|
+
<li>Apple and Google, for sign-in, payments, and app distribution.</li>
|
|
72
|
+
<li>RevenueCat, to tell us whether a subscription is active.</li>
|
|
73
|
+
<li>Our hosting provider, which stores the data and serves the API.</li>
|
|
74
|
+
<li>Our email provider, to deliver sign-in codes.</li>
|
|
75
|
+
</ul>
|
|
76
|
+
|
|
77
|
+
<h2>How long we keep it</h2>
|
|
78
|
+
<p>
|
|
79
|
+
Your notes stay until you delete them or delete your account. Logs are
|
|
80
|
+
kept for a short period and then discarded.
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<h2>Deleting your account</h2>
|
|
84
|
+
<p>
|
|
85
|
+
Open the app and go to Settings, then Delete account. This removes your
|
|
86
|
+
account and the notes attached to it. It happens immediately and it
|
|
87
|
+
cannot be undone. A subscription is billed by the store, so cancel it
|
|
88
|
+
in your App Store or Play Store account as well.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<h2>Your rights</h2>
|
|
92
|
+
<p>
|
|
93
|
+
You can ask what we hold about you, ask for a copy, ask us to correct
|
|
94
|
+
it, or ask us to delete it. Write to us at the address below. Depending
|
|
95
|
+
on where you live, you may have further rights under local law.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<h2>Children</h2>
|
|
99
|
+
<p>
|
|
100
|
+
{site.name} is not directed at children under 13, and we do not
|
|
101
|
+
knowingly collect their data. If you believe a child has given us
|
|
102
|
+
information, contact us and we will remove it.
|
|
103
|
+
</p>
|
|
104
|
+
|
|
105
|
+
<h2>Security</h2>
|
|
106
|
+
<p>
|
|
107
|
+
Traffic between the app and our servers is encrypted in transit. We
|
|
108
|
+
limit who can reach production data. No service can promise perfect
|
|
109
|
+
security, and we do not.
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<h2>Changes</h2>
|
|
113
|
+
<p>
|
|
114
|
+
If this policy changes in a way that matters, we will update the date
|
|
115
|
+
at the top of this page and, where the change is significant, tell you
|
|
116
|
+
in the app.
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<h2>Contact</h2>
|
|
120
|
+
<p>
|
|
121
|
+
<Company />
|
|
122
|
+
{site.address ? <>, {site.address}</> : null}
|
|
123
|
+
{site.supportEmail ? (
|
|
124
|
+
<>
|
|
125
|
+
. Email <a href={`mailto:${site.supportEmail}`}>{site.supportEmail}</a>.
|
|
126
|
+
</>
|
|
127
|
+
) : null}
|
|
128
|
+
</p>
|
|
129
|
+
</LegalPage>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { site } from "@/lib/site";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: `Support — ${site.name}`,
|
|
7
|
+
description: `Get help with ${site.name}.`,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* App Store Connect asks for a support URL, and Play requires a contact
|
|
12
|
+
* address on the listing. This page is both. The answers repeat what the app
|
|
13
|
+
* does so a reviewer can check a claim without installing anything.
|
|
14
|
+
*/
|
|
15
|
+
export default function SupportPage() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="mx-auto max-w-3xl px-6 py-16 sm:py-20">
|
|
18
|
+
<h1 className="text-3xl font-semibold tracking-[-0.02em]">Support</h1>
|
|
19
|
+
<p className="mt-4 text-[15px] leading-relaxed text-ink-muted">
|
|
20
|
+
{site.supportEmail ? (
|
|
21
|
+
<>
|
|
22
|
+
Email{" "}
|
|
23
|
+
<a
|
|
24
|
+
href={`mailto:${site.supportEmail}`}
|
|
25
|
+
className="text-brand underline underline-offset-2"
|
|
26
|
+
>
|
|
27
|
+
{site.supportEmail}
|
|
28
|
+
</a>{" "}
|
|
29
|
+
and tell us what happened, what you expected, and which device you
|
|
30
|
+
are on. That is usually enough to sort it out in one reply.
|
|
31
|
+
</>
|
|
32
|
+
) : (
|
|
33
|
+
<>Add a support address in lib/site.ts and it will appear here.</>
|
|
34
|
+
)}
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h2 className="mt-12 text-xl font-semibold tracking-[-0.015em]">
|
|
38
|
+
Common questions
|
|
39
|
+
</h2>
|
|
40
|
+
<dl className="mt-6 divide-y divide-line border-y border-line">
|
|
41
|
+
{site.faq.map((item) => (
|
|
42
|
+
<div key={item.q} className="py-5">
|
|
43
|
+
<dt className="text-[15px] font-medium">{item.q}</dt>
|
|
44
|
+
<dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
45
|
+
{item.a}
|
|
46
|
+
</dd>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
<div className="py-5">
|
|
50
|
+
<dt className="text-[15px] font-medium">
|
|
51
|
+
How do I delete my account?
|
|
52
|
+
</dt>
|
|
53
|
+
<dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
54
|
+
In the app, open Settings and choose Delete account. It removes the
|
|
55
|
+
account and its notes straight away. Cancel a subscription
|
|
56
|
+
separately in the App Store or Play Store.
|
|
57
|
+
</dd>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="py-5">
|
|
60
|
+
<dt className="text-[15px] font-medium">
|
|
61
|
+
My notes are not showing on my other device.
|
|
62
|
+
</dt>
|
|
63
|
+
<dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
|
|
64
|
+
Check that both devices are signed in to the same account. Notes
|
|
65
|
+
written before you signed in move to the account the first time you
|
|
66
|
+
sign in on that device.
|
|
67
|
+
</dd>
|
|
68
|
+
</div>
|
|
69
|
+
</dl>
|
|
70
|
+
|
|
71
|
+
<p className="mt-10 text-[14px] text-ink-muted">
|
|
72
|
+
See also our <Link href="/privacy" className="text-brand underline underline-offset-2">privacy policy</Link>{" "}
|
|
73
|
+
and{" "}
|
|
74
|
+
<Link href="/terms" className="text-brand underline underline-offset-2">terms</Link>.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|