@pylonsync/create-pylon 0.9.1 → 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.
Files changed (24) hide show
  1. package/package.json +1 -1
  2. package/templates/backend/mobile/apps/api/README.md +20 -1
  3. package/templates/backend/mobile/apps/api/app/(site)/layout.tsx +103 -0
  4. package/templates/backend/mobile/apps/api/app/(site)/not-found.tsx +23 -0
  5. package/templates/backend/mobile/apps/api/app/(site)/page.tsx +221 -0
  6. package/templates/backend/mobile/apps/api/app/(site)/privacy/page.tsx +131 -0
  7. package/templates/backend/mobile/apps/api/app/(site)/support/page.tsx +78 -0
  8. package/templates/backend/mobile/apps/api/app/(site)/terms/page.tsx +144 -0
  9. package/templates/backend/mobile/apps/api/app/error.tsx +27 -0
  10. package/templates/backend/mobile/apps/api/app/globals.css +87 -0
  11. package/templates/backend/mobile/apps/api/app/layout.tsx +27 -0
  12. package/templates/backend/mobile/apps/api/app/robots.ts +13 -0
  13. package/templates/backend/mobile/apps/api/app/sitemap.ts +16 -0
  14. package/templates/backend/mobile/apps/api/app.ts +6 -2
  15. package/templates/backend/mobile/apps/api/components/legal.tsx +64 -0
  16. package/templates/backend/mobile/apps/api/lib/site.ts +94 -0
  17. package/templates/backend/mobile/apps/api/package.json +8 -1
  18. package/templates/backend/mobile/apps/api/tsconfig.json +6 -2
  19. package/templates/expo/mobile/apps/expo/.env.example +13 -4
  20. package/templates/expo/mobile/apps/expo/README.md +9 -0
  21. package/templates/expo/mobile/apps/expo/STORE.md +9 -5
  22. package/templates/expo/mobile/apps/expo/app/(tabs)/settings.tsx +9 -6
  23. package/templates/expo/mobile/apps/expo/app/paywall.tsx +9 -13
  24. package/templates/expo/mobile/apps/expo/src/links.ts +29 -0
@@ -0,0 +1,144 @@
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: `Terms of Service — ${site.name}`,
8
+ description: `The agreement between you and ${site.name}.`,
9
+ };
10
+
11
+ /**
12
+ * The terms the app links to from the paywall and from Settings. Apple
13
+ * requires subscription terms to be visible next to the purchase, and this
14
+ * page is where the detail lives. Keep the billing section true to what your
15
+ * products actually do.
16
+ */
17
+ export default function TermsPage() {
18
+ return (
19
+ <LegalPage title="Terms of Service">
20
+ <p>
21
+ These terms are the agreement between you and <Company /> for the use
22
+ of {site.name}. Using the app means you accept them.
23
+ </p>
24
+
25
+ <h2>Who may use it</h2>
26
+ <p>
27
+ You need to be at least 13 years old. If you use {site.name} for an
28
+ organization, you confirm you may accept these terms for it.
29
+ </p>
30
+
31
+ <h2>Your account</h2>
32
+ <p>
33
+ You are responsible for what happens under your account, so keep
34
+ access to your email and devices secure. Tell us if you think someone
35
+ else has your account.
36
+ </p>
37
+
38
+ <h2>Free and paid</h2>
39
+ <p>
40
+ The free tier holds up to {site.pricing.freeLimit} notes. A
41
+ subscription lifts that limit and includes what we add later. We may
42
+ change what each tier includes, and we will not reduce what you have
43
+ already paid for during a period you have paid.
44
+ </p>
45
+
46
+ <h2>Billing</h2>
47
+ <ul>
48
+ <li>
49
+ Apple and Google sell and bill the subscription, not us. The price
50
+ you see in the app is the price they charge in your country.
51
+ </li>
52
+ <li>
53
+ A subscription renews automatically until you cancel it. Cancel at
54
+ least 24 hours before the period ends, in your App Store or Play
55
+ Store account settings.
56
+ </li>
57
+ <li>
58
+ Refunds follow the store&apos;s policy. We cannot issue a refund for
59
+ a purchase we did not process, so ask Apple or Google.
60
+ </li>
61
+ <li>
62
+ Deleting the app does not cancel a subscription. Cancel it in the
63
+ store.
64
+ </li>
65
+ </ul>
66
+
67
+ <h2>Your content</h2>
68
+ <p>
69
+ Your notes are yours. You give us only the permission we need to run
70
+ the service, which is to store your content, back it up, and send it to
71
+ the devices you sign in on. We claim no ownership of it and we do not
72
+ use it to train anything.
73
+ </p>
74
+ <p>
75
+ You are responsible for what you put in the app, and for having the
76
+ right to put it there.
77
+ </p>
78
+
79
+ <h2>Acceptable use</h2>
80
+ <p>Do not use {site.name} to:</p>
81
+ <ul>
82
+ <li>break the law, or store material that is illegal to hold;</li>
83
+ <li>
84
+ attack the service, work around its limits, or get at other
85
+ people&apos;s data;
86
+ </li>
87
+ <li>resell it as your own product.</li>
88
+ </ul>
89
+
90
+ <h2>Availability</h2>
91
+ <p>
92
+ We work to keep {site.name} running, but we do not promise it will be
93
+ available without interruption. We may change or stop features. If we
94
+ shut the service down, we will give reasonable notice and a way to
95
+ export your notes.
96
+ </p>
97
+
98
+ <h2>Ending it</h2>
99
+ <p>
100
+ You can stop at any time by deleting your account in Settings. We may
101
+ suspend or close an account that breaks these terms or puts the service
102
+ or other people at risk.
103
+ </p>
104
+
105
+ <h2>Disclaimer and liability</h2>
106
+ <p>
107
+ {site.name} is provided as it is, without warranties beyond those the
108
+ law requires. To the extent the law allows, <Company /> is not liable
109
+ for indirect or consequential loss, and our total liability is limited
110
+ to what you paid us in the 12 months before the claim.
111
+ </p>
112
+ <p>
113
+ Nothing here removes rights you have as a consumer that cannot be
114
+ removed by agreement.
115
+ </p>
116
+
117
+ <h2>Changes</h2>
118
+ <p>
119
+ We may update these terms. The date at the top shows the current
120
+ version, and we will tell you in the app about a significant change.
121
+ Continuing to use {site.name} after that means you accept the new
122
+ terms.
123
+ </p>
124
+
125
+ <h2>Governing law</h2>
126
+ <p>
127
+ These terms are governed by the laws of{" "}
128
+ {site.governingLaw || "[your jurisdiction]"}, without regard to
129
+ conflict of law rules.
130
+ </p>
131
+
132
+ <h2>Contact</h2>
133
+ <p>
134
+ <Company />
135
+ {site.address ? <>, {site.address}</> : null}
136
+ {site.supportEmail ? (
137
+ <>
138
+ . Email <a href={`mailto:${site.supportEmail}`}>{site.supportEmail}</a>.
139
+ </>
140
+ ) : null}
141
+ </p>
142
+ </LegalPage>
143
+ );
144
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+
4
+ // The error boundary sits at the root, outside the route group, so it still
5
+ // renders if a layout itself throws. It is hydrated, so `reset()` works.
6
+ export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
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
+ Something went wrong
11
+ </h1>
12
+ <p className="mt-3 text-[15px] text-ink-muted">{error.message}</p>
13
+ {error.digest ? (
14
+ <p className="mt-1 text-[12px] text-ink-muted">
15
+ Reference: <code className="font-mono">{error.digest}</code>
16
+ </p>
17
+ ) : null}
18
+ <button
19
+ type="button"
20
+ onClick={reset}
21
+ className="mt-8 rounded-xl bg-ink px-5 py-3 text-[14px] font-medium text-surface transition-opacity hover:opacity-90"
22
+ >
23
+ Try again
24
+ </button>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,87 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Tailwind v4 scans these globs for class names. Add a line if you put
4
+ markup somewhere else. */
5
+ @source "../app/**/*.{tsx,ts,jsx,js}";
6
+ @source "../components/**/*.{tsx,ts,jsx,js}";
7
+ @source "../lib/**/*.{tsx,ts,jsx,js}";
8
+
9
+ /* The whole site re-themes from these. `--brand` is the accent on buttons
10
+ and links; the ink/surface pair carries everything else. */
11
+ :root {
12
+ --brand: #4f46e5;
13
+ --brand-ink: #ffffff;
14
+ --brand-soft: #eef2ff;
15
+ --ink: #18181b;
16
+ --ink-muted: #52525b;
17
+ --surface: #ffffff;
18
+ --surface-soft: #fafafa;
19
+ --line: #e4e4e7;
20
+ }
21
+
22
+ @media (prefers-color-scheme: dark) {
23
+ :root {
24
+ --brand: #818cf8;
25
+ --brand-ink: #1e1b4b;
26
+ --brand-soft: #1e1b4b;
27
+ --ink: #fafafa;
28
+ --ink-muted: #a1a1aa;
29
+ --surface: #09090b;
30
+ --surface-soft: #131316;
31
+ --line: #27272a;
32
+ }
33
+ }
34
+
35
+ @theme inline {
36
+ --color-brand: var(--brand);
37
+ --color-brand-ink: var(--brand-ink);
38
+ --color-brand-soft: var(--brand-soft);
39
+ --color-ink: var(--ink);
40
+ --color-ink-muted: var(--ink-muted);
41
+ --color-surface: var(--surface);
42
+ --color-surface-soft: var(--surface-soft);
43
+ --color-line: var(--line);
44
+ --font-sans:
45
+ ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
46
+ "Helvetica Neue", Arial, sans-serif;
47
+ }
48
+
49
+ html {
50
+ -webkit-text-size-adjust: 100%;
51
+ }
52
+
53
+ body {
54
+ font-family: var(--font-sans);
55
+ -webkit-font-smoothing: antialiased;
56
+ text-rendering: optimizeLegibility;
57
+ }
58
+
59
+ /* Long-form legal copy. The legal pages are plain prose, so style the
60
+ elements once here instead of putting a class on every paragraph. */
61
+ .prose-legal > * + * {
62
+ margin-top: 1rem;
63
+ }
64
+ .prose-legal h2 {
65
+ margin-top: 2.5rem;
66
+ font-size: 1.125rem;
67
+ font-weight: 600;
68
+ letter-spacing: -0.01em;
69
+ color: var(--ink);
70
+ }
71
+ .prose-legal p,
72
+ .prose-legal li {
73
+ color: var(--ink-muted);
74
+ line-height: 1.7;
75
+ }
76
+ .prose-legal ul {
77
+ margin-left: 1.125rem;
78
+ list-style: disc;
79
+ }
80
+ .prose-legal li + li {
81
+ margin-top: 0.375rem;
82
+ }
83
+ .prose-legal a {
84
+ color: var(--brand);
85
+ text-decoration: underline;
86
+ text-underline-offset: 2px;
87
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+
3
+ interface LayoutProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ /**
8
+ * The document shell. Chrome lives in the section layout
9
+ * (`app/(site)/layout.tsx`), which is what wraps the pages with the header
10
+ * and footer. Tailwind's compiled stylesheet is injected into <head> by the
11
+ * runtime.
12
+ *
13
+ * No <title> here on purpose: each page exports its own `metadata`, and a
14
+ * title in the layout would render first and win.
15
+ */
16
+ export default function RootLayout({ children }: LayoutProps) {
17
+ return (
18
+ <html lang="en">
19
+ <head>
20
+ <meta charSet="utf-8" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
22
+ <meta name="color-scheme" content="light dark" />
23
+ </head>
24
+ <body className="min-h-screen bg-surface text-ink">{children}</body>
25
+ </html>
26
+ );
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+ import { site } from "@/lib/site";
3
+
4
+ // app/robots.ts → served at /robots.txt.
5
+ const SITE = site.url || process.env.SITE_URL || "http://localhost:4321";
6
+
7
+ export default function robots(): Robots {
8
+ return {
9
+ // The API is not for crawlers; the marketing and legal pages are.
10
+ rules: { userAgent: "*", allow: "/", disallow: ["/api/"] },
11
+ sitemap: `${SITE}/sitemap.xml`,
12
+ };
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+ import { site } from "@/lib/site";
3
+
4
+ // app/sitemap.ts → served at /sitemap.xml. This is a server-only route
5
+ // module, never bundled for the browser, so reading process.env here is
6
+ // safe (page components cannot).
7
+ const SITE = site.url || process.env.SITE_URL || "http://localhost:4321";
8
+
9
+ export default function sitemap(): Sitemap {
10
+ return [
11
+ { url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
12
+ { url: `${SITE}/support`, changeFrequency: "monthly", priority: 0.5 },
13
+ { url: `${SITE}/privacy`, changeFrequency: "yearly", priority: 0.3 },
14
+ { url: `${SITE}/terms`, changeFrequency: "yearly", priority: 0.3 },
15
+ ];
16
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  auth,
3
3
  buildManifest,
4
+ discoverAppRoutes,
4
5
  discoverFunctions,
5
6
  entity,
6
7
  field,
@@ -73,8 +74,11 @@ const manifest = buildManifest({
73
74
  // register each twice.
74
75
  actions: fns.actions,
75
76
  policies: [userPolicy, notePolicy, ...purchases.manifest.policies],
76
- // API only: the Expo app is the frontend.
77
- routes: [],
77
+ // The public website: landing page, support, privacy policy, and terms,
78
+ // under `app/`. One Pylon server answers the Expo app's API calls AND
79
+ // serves these pages, so the legal URLs both stores demand are live on the
80
+ // same host as soon as you deploy. Content lives in `lib/site.ts`.
81
+ routes: await discoverAppRoutes(),
78
82
  // Email/password + magic codes are on by default. Native sign-in needs
79
83
  // the app's ids on the server: PYLON_APPLE_NATIVE_CLIENT_IDS (the iOS
80
84
  // bundle id) and PYLON_GOOGLE_NATIVE_CLIENT_IDS (the iOS + Android OAuth
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import { site } from "@/lib/site";
3
+
4
+ /**
5
+ * The frame both legal pages share: title, effective date, and the notice
6
+ * that this text is a starting point rather than reviewed legal advice.
7
+ *
8
+ * The notice is shown until `lib/site.ts` has your company details, on the
9
+ * same principle as the banner in the site layout: unfinished legal text
10
+ * should say so.
11
+ */
12
+ export function LegalPage({
13
+ title,
14
+ children,
15
+ }: {
16
+ title: string;
17
+ children: React.ReactNode;
18
+ }) {
19
+ const unfinished = !site.company || !site.supportEmail || !site.address;
20
+ return (
21
+ <article className="mx-auto max-w-3xl px-6 py-16 sm:py-20">
22
+ <h1 className="text-3xl font-semibold tracking-[-0.02em]">{title}</h1>
23
+ <p className="mt-3 text-[13px] text-ink-muted">
24
+ Effective {formatDate(site.legalEffective)}
25
+ {site.company ? ` · ${site.company}` : ""}
26
+ </p>
27
+ {unfinished ? (
28
+ <p className="mt-8 rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-[13px] leading-relaxed text-amber-900">
29
+ Draft. This text describes what the app in this template actually
30
+ does, which makes it a useful starting point, but it has not been
31
+ reviewed by a lawyer and it does not know your business. Fill in
32
+ <code className="mx-1 font-mono">lib/site.ts</code>, read every
33
+ section, and have counsel check it before you submit to a store.
34
+ </p>
35
+ ) : null}
36
+ <div className="prose-legal mt-10">{children}</div>
37
+ <p className="mt-12 border-t border-line pt-6 text-[13px] text-ink-muted">
38
+ Questions about this page?{" "}
39
+ {site.supportEmail ? (
40
+ <a href={`mailto:${site.supportEmail}`}>{site.supportEmail}</a>
41
+ ) : (
42
+ "Add a support address in lib/site.ts."
43
+ )}
44
+ </p>
45
+ </article>
46
+ );
47
+ }
48
+
49
+ /** The legal entity, or a visible placeholder while the config is blank. */
50
+ export function Company() {
51
+ return <>{site.company || "[your company]"}</>;
52
+ }
53
+
54
+ function formatDate(iso: string): string {
55
+ // Build from the parts. `new Date("2026-01-01")` parses as UTC midnight and
56
+ // renders as the day before in any negative-UTC timezone.
57
+ const [y, m, d] = iso.split("-").map(Number);
58
+ if (!y || !m || !d) return iso;
59
+ return new Date(y, m - 1, d).toLocaleDateString("en-US", {
60
+ year: "numeric",
61
+ month: "long",
62
+ day: "numeric",
63
+ });
64
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Everything the marketing and legal pages read. This is the one file to
3
+ * edit when you rename the app, add store links, or fill in your company
4
+ * details.
5
+ *
6
+ * It is a plain module, not env vars, on purpose: SSR pages are bundled for
7
+ * hydration and the bundler rewrites `process.env.*`, so page code cannot
8
+ * read arbitrary env at render time. Server-only route modules
9
+ * (app/sitemap.ts, app/robots.ts) can, and do.
10
+ */
11
+
12
+ export const site = {
13
+ /** Shown in the header, the page titles, and the legal pages. */
14
+ name: "__APP_NAME__",
15
+ /** The hero headline. One sentence, what the app does for the person. */
16
+ tagline: "Your notes, on every device, the moment you write them",
17
+ /** The line under the headline, and the meta description. */
18
+ description:
19
+ "__APP_NAME__ keeps your notes in sync across your phone, tablet, and anything else you sign in on. It works offline and catches up when you are back.",
20
+
21
+ /**
22
+ * The public URL of this site once deployed, with no trailing slash.
23
+ * `pylon deploy` prints the host. Used for canonical links and the
24
+ * sitemap, and by the mobile app when it opens the legal pages.
25
+ */
26
+ url: "",
27
+
28
+ /** The legal entity that publishes the app. Appears on both legal pages. */
29
+ company: "",
30
+ /** Where support email and legal notices go. */
31
+ supportEmail: "",
32
+ /** Postal address. Apple asks for one on the privacy policy. */
33
+ address: "",
34
+ /** The law that governs the terms, e.g. "the State of Texas, USA". */
35
+ governingLaw: "",
36
+ /** Date the current legal text took effect, as YYYY-MM-DD. */
37
+ legalEffective: "2026-01-01",
38
+
39
+ /** Store links. Empty renders a "coming soon" state instead of a dead link. */
40
+ appStoreUrl: "",
41
+ playStoreUrl: "",
42
+
43
+ /** The three or four things worth saying on the landing page. */
44
+ features: [
45
+ {
46
+ title: "Write now, sync later",
47
+ body: "Notes save on your device first, so the app never waits on a network. They reach your other devices as soon as there is one.",
48
+ },
49
+ {
50
+ title: "Start without an account",
51
+ body: "Open the app and use it. Sign in when you want your notes on a second device, and everything you already wrote comes with you.",
52
+ },
53
+ {
54
+ title: "Yours to delete",
55
+ body: "Delete your account in Settings and your notes go with it. No support ticket, no waiting.",
56
+ },
57
+ ],
58
+
59
+ /** Free tier and subscription, kept honest with what the app enforces. */
60
+ pricing: {
61
+ freeLimit: 10,
62
+ proBlurb: "Unlimited notes and everything shipped next.",
63
+ },
64
+
65
+ faq: [
66
+ {
67
+ q: "Do I need an account?",
68
+ a: "No. The app works straight away. Signing in keeps your notes if you switch phones.",
69
+ },
70
+ {
71
+ q: "How do I cancel a subscription?",
72
+ a: "Subscriptions are billed by Apple or Google, so cancel from your App Store or Play Store account. Access lasts until the end of the period you paid for.",
73
+ },
74
+ {
75
+ q: "What happens to my notes if I delete my account?",
76
+ a: "They are deleted with it. Settings has the button, and it takes effect immediately.",
77
+ },
78
+ ],
79
+ } as const;
80
+
81
+ /**
82
+ * Config the site cannot honestly render without. The header shows a notice
83
+ * while any of these are blank, so an unfinished site cannot quietly go live
84
+ * with placeholder legal text.
85
+ */
86
+ export function missingSiteConfig(): string[] {
87
+ const missing: string[] = [];
88
+ if (!site.url) missing.push("url");
89
+ if (!site.company) missing.push("company");
90
+ if (!site.supportEmail) missing.push("supportEmail");
91
+ if (!site.address) missing.push("address");
92
+ if (!site.governingLaw) missing.push("governingLaw");
93
+ return missing;
94
+ }
@@ -13,11 +13,18 @@
13
13
  "dependencies": {
14
14
  "@pylonsync/sdk": "^__PYLON_VERSION__",
15
15
  "@pylonsync/functions": "^__PYLON_VERSION__",
16
- "@pylonsync/revenuecat": "^__PYLON_VERSION__"
16
+ "@pylonsync/react": "^__PYLON_VERSION__",
17
+ "@pylonsync/revenuecat": "^__PYLON_VERSION__",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0",
20
+ "tailwindcss": "^4.3.0",
21
+ "@tailwindcss/cli": "^4.3.0"
17
22
  },
18
23
  "devDependencies": {
19
24
  "@pylonsync/cli": "^__PYLON_VERSION__",
20
25
  "@types/node": "^22.0.0",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
21
28
  "typescript": "^5.6.0"
22
29
  }
23
30
  }
@@ -3,11 +3,15 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
+ "jsx": "react",
6
7
  "strict": true,
7
8
  "esModuleInterop": true,
8
9
  "skipLibCheck": true,
9
10
  "noEmit": true,
10
- "types": ["node"]
11
+ "lib": ["ES2022", "DOM"],
12
+ "types": ["react", "react-dom", "node"],
13
+ "baseUrl": ".",
14
+ "paths": { "@/*": ["./*"] }
11
15
  },
12
- "include": ["app.ts", "functions", "lib"]
16
+ "include": ["app.ts", "app/**/*", "components/**/*", "functions/**/*", "lib/**/*"]
13
17
  }
@@ -13,7 +13,16 @@ EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=
13
13
  EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=
14
14
  EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=
15
15
 
16
- # Shown in Settings and required by both stores.
17
- EXPO_PUBLIC_PRIVACY_URL=https://example.com/privacy
18
- EXPO_PUBLIC_TERMS_URL=https://example.com/terms
19
- EXPO_PUBLIC_SUPPORT_EMAIL=support@example.com
16
+ # The privacy policy, terms, and help pages are served by the backend in
17
+ # apps/api, so they default to EXPO_PUBLIC_PYLON_BASE_URL + /privacy, /terms,
18
+ # and /support. Both stores require a reachable privacy policy URL.
19
+ #
20
+ # Set these only if the marketing site lives somewhere else, e.g. your own
21
+ # domain. EXPO_PUBLIC_SITE_URL moves all three at once.
22
+ # EXPO_PUBLIC_SITE_URL=https://yourapp.com
23
+ # EXPO_PUBLIC_PRIVACY_URL=
24
+ # EXPO_PUBLIC_TERMS_URL=
25
+ # EXPO_PUBLIC_SUPPORT_URL=
26
+
27
+ # Optional. Adds a "Contact support" row in Settings.
28
+ EXPO_PUBLIC_SUPPORT_EMAIL=
@@ -15,9 +15,16 @@ src/session.tsx boot + state machine
15
15
  src/purchases.ts RevenueCat wrapper, safe in Expo Go
16
16
  src/entitlements.ts usePro() from the synced RcEntitlement rows
17
17
  src/analytics.ts funnel events; wire to your SDK in one place
18
+ src/links.ts privacy / terms / support URLs, served by apps/api
18
19
  STORE.md the submission checklist
19
20
  ```
20
21
 
22
+ The public website (landing page, `/privacy`, `/terms`, `/support`) is served
23
+ by the backend in `apps/api`, on the same host as the API. The links in
24
+ Settings and under the paywall point there with no configuration, which is
25
+ what makes the store requirement satisfiable from a fresh scaffold. Edit its
26
+ copy in `apps/api/lib/site.ts`.
27
+
21
28
  ## Run
22
29
 
23
30
  ```bash
@@ -49,6 +56,8 @@ also carry Notes-specific copy or logic:
49
56
  - `app/(tabs)/settings.tsx`: the delete-account confirmation.
50
57
  - `app/(tabs)/index.tsx`: the list, `FREE_LIMIT`, and the sign-in nudge.
51
58
  - `app.config.ts`: `name`, `slug`, `scheme`, and the icons in `assets/`.
59
+ - `apps/api/lib/site.ts`: the website copy, store links, and the company
60
+ details the privacy policy and terms need.
52
61
  - `apps/api/functions/deleteMyData.ts`: delete every entity that stores
53
62
  user data, or account deletion leaves rows behind.
54
63
  - `apps/api/functions/createNote.ts`: the free-tier cap.
@@ -3,7 +3,7 @@
3
3
  The order below is the shortest path that App Review and Google Play accept
4
4
  on the first try. Each step is one command or one dashboard page.
5
5
 
6
- ## 1. Backend live
6
+ ## 1. Backend and website live
7
7
 
8
8
  ```bash
9
9
  cd apps/api && pylon deploy
@@ -52,8 +52,12 @@ Test the purchase on TestFlight with a sandbox Apple ID before submitting.
52
52
 
53
53
  Both stores require, and reviewers check:
54
54
 
55
- - Privacy policy URL and terms URL (`EXPO_PUBLIC_PRIVACY_URL`, `EXPO_PUBLIC_TERMS_URL`).
56
- The app shows them in Settings and under the paywall.
55
+ - Privacy policy URL and terms URL. The backend serves them at
56
+ `<your-backend>/privacy` and `<your-backend>/terms`, and the app links to
57
+ them from Settings and under the paywall with no configuration. Fill in
58
+ `apps/api/lib/site.ts` first: the text ships as a draft and a banner stays
59
+ on the page until you do.
60
+ - A support URL for App Store Connect: `<your-backend>/support`.
57
61
  - Account deletion inside the app (Settings → Delete account). Present.
58
62
  - Sign in with Apple when any other third-party sign-in is offered. Present.
59
63
  - Subscription terms next to the purchase button (price, period, renewal). Present.
@@ -87,8 +91,8 @@ Without it `eas submit` asks for the app interactively.
87
91
  RevenueCat, and the iOS and Android public keys are in `apps/expo/.env`.
88
92
  - Sign-in: `PYLON_APPLE_NATIVE_CLIENT_IDS` (the bundle id) and
89
93
  `PYLON_GOOGLE_NATIVE_CLIENT_IDS` are set on the backend.
90
- - Legal: `EXPO_PUBLIC_PRIVACY_URL`, `EXPO_PUBLIC_TERMS_URL`, and
91
- `EXPO_PUBLIC_SUPPORT_EMAIL` point at real pages and a monitored inbox.
94
+ - Legal: `apps/api/lib/site.ts` is filled in, the draft banner is gone from
95
+ `/privacy` and `/terms`, and a lawyer has read both.
92
96
  - Deletion: `apps/api/functions/deleteMyData.ts` removes every entity that
93
97
  stores user data. Test it with a throwaway account.
94
98
  - Copy: the "Replace the demo" list in `README.md` is done.
@@ -4,6 +4,7 @@ import { useRouter } from "expo-router";
4
4
  import Constants from "expo-constants";
5
5
  import { deleteAccount } from "@pylonsync/react-native";
6
6
  import { track } from "@/analytics";
7
+ import { PRIVACY_URL, SUPPORT_EMAIL, SUPPORT_URL, TERMS_URL } from "@/links";
7
8
  import { usePro } from "@/entitlements";
8
9
  import { resetFlags } from "@/flags";
9
10
  import { manageSubscriptionUrl, restore } from "@/purchases";
@@ -27,9 +28,8 @@ export default function Settings() {
27
28
  const { pro } = usePro();
28
29
  const [busy, setBusy] = useState<string | null>(null);
29
30
 
30
- const privacy = process.env.EXPO_PUBLIC_PRIVACY_URL;
31
- const terms = process.env.EXPO_PUBLIC_TERMS_URL;
32
- const support = process.env.EXPO_PUBLIC_SUPPORT_EMAIL;
31
+ // These resolve to the site the backend serves (apps/api), so the rows
32
+ // always render and always lead somewhere real. See src/links.ts.
33
33
  const version = Constants.expoConfig?.version ?? "dev";
34
34
 
35
35
  async function restorePurchases() {
@@ -99,9 +99,12 @@ export default function Settings() {
99
99
  <Spacer />
100
100
 
101
101
  <Caption>About</Caption>
102
- {privacy ? <Row label="Privacy policy" onPress={() => void Linking.openURL(privacy)} /> : null}
103
- {terms ? <Row label="Terms of service" onPress={() => void Linking.openURL(terms)} /> : null}
104
- {support ? <Row label="Contact support" onPress={() => void Linking.openURL(`mailto:${support}`)} /> : null}
102
+ <Row label="Help" onPress={() => void Linking.openURL(SUPPORT_URL)} />
103
+ <Row label="Privacy policy" onPress={() => void Linking.openURL(PRIVACY_URL)} />
104
+ <Row label="Terms of service" onPress={() => void Linking.openURL(TERMS_URL)} />
105
+ {SUPPORT_EMAIL ? (
106
+ <Row label="Contact support" onPress={() => void Linking.openURL(`mailto:${SUPPORT_EMAIL}`)} />
107
+ ) : null}
105
108
  <Row label="Version" value={version} />
106
109
  <Spacer />
107
110