+
+
+
+
- Ready to start
+ Ready when you are
-
- Create your account today and get 2 weeks for free.
+
+ Send your first note of thanks today.
-
- No credit card required to get started.
+
+ Your first 14 days are on us. No credit card required.
-
-
-
+
diff --git a/app/routes/_app+/_marketing+/privacy.tsx b/app/routes/_app+/_marketing+/privacy.tsx
index 4cba5721..13c23393 100644
--- a/app/routes/_app+/_marketing+/privacy.tsx
+++ b/app/routes/_app+/_marketing+/privacy.tsx
@@ -1,7 +1,193 @@
+import { type MetaFunction } from 'react-router'
+import {
+ ProsePage,
+ ProseSection,
+ ProseTableOfContents,
+} from '#app/components/prose-page.tsx'
+
+export const meta: MetaFunction = () => [
+ { title: 'Privacy Policy | GratiText' },
+ {
+ name: 'description',
+ content:
+ 'How GratiText collects, uses, and protects the information you and your recipients share with us.',
+ },
+]
+
+const sections = [
+ { id: 'what-we-collect', title: 'What we collect' },
+ { id: 'how-we-use-it', title: 'How we use it' },
+ { id: 'text-messages', title: 'Text messages and consent' },
+ { id: 'sharing', title: 'Who we share it with' },
+ { id: 'retention', title: 'Retention and deletion' },
+ { id: 'security', title: 'Security' },
+ { id: 'your-choices', title: 'Your choices' },
+ { id: 'children', title: 'Children' },
+ { id: 'changes', title: 'Changes to this policy' },
+ { id: 'contact', title: 'Contact' },
+]
+
export default function PrivacyRoute() {
return (
-
-
Privacy
-
+
+
+
+
+
+ Account details. When you sign up we collect your
+ phone number, a username, your name, and a password (stored only as a
+ salted hash). If you turn on two-factor authentication we store the
+ secret needed to verify your codes.
+
+
+ Recipients. To deliver your notes we store the name,
+ phone number, time zone, and sending schedule for each person you add.
+
+
+ Messages. We store the messages you write, when they
+ are scheduled, and when they were sent so you can review your history.
+
+
+ Billing. Payments are processed by Stripe. We keep a
+ reference to your Stripe customer record and your subscription status.
+ We never see or store your full card number.
+
+
+ Technical data. Like most services we keep standard
+ server logs (such as IP address, browser type, and pages requested)
+ and error reports so we can keep GratiText reliable and secure.
+
+
+
+
+
+ -
+ To create and secure your account and verify your phone number.
+
+ -
+ To send your messages to your recipients on the schedule you set.
+
+ -
+ To remind you when a recipient has no message waiting to be sent.
+
+ - To process payments and manage your subscription.
+ - To respond to your support requests.
+ - To detect abuse and keep the service running smoothly.
+
+
+ We do not use your messages to train AI models, build advertising
+ profiles, or for any purpose other than delivering them as you asked.
+
+
+
+
+
+ GratiText sends SMS messages through Twilio. Before we deliver your
+ notes to a new recipient, we text them a one-time verification code so
+ they know who is reaching out and can confirm they are happy to
+ receive messages from you.
+
+
+ Any recipient can reply STOP at any time to opt out
+ of all future messages, and START to opt back in.
+ Standard message and data rates from their carrier may apply.
+
+
+
+
+
+ We share information only with the service providers that help us run
+ GratiText, and only as needed for them to do their job:
+
+
+ -
+ Twilio to deliver text messages and receive
+ replies.
+
+ -
+ Stripe to process payments and manage
+ subscriptions.
+
+ -
+ Fly.io to host the application and its database.
+
+ -
+ Sentry to capture error reports so we can fix
+ problems quickly.
+
+
+
+ We may also disclose information when required by law or to protect
+ the rights and safety of our users. We do not sell personal
+ information.
+
+
+
+
+
+ We keep your information for as long as your account is active. You
+ can delete your account at any time from{' '}
+ your settings page; doing so removes
+ your account, recipients, and messages from our database. Copies in
+ routine backups and logs age out on a rolling schedule after that.
+
+
+ You can also download a copy of everything we hold about you from the
+ same page.
+
+
+
+
+
+ All traffic to GratiText is encrypted in transit. Passwords are hashed
+ with bcrypt, sessions are stored in signed cookies, and access to
+ production systems is limited to the people who operate the service.
+ No system is perfectly secure, so please use a strong, unique password
+ and consider enabling two-factor authentication.
+
+
+
+
+
+ -
+ Update your name, username, and phone number in your settings.
+
+ - Pause or delete any recipient whenever you like.
+ -
+ Cancel your subscription at any time through the billing portal.
+
+ - Download or delete all of your data from your settings page.
+
+
+
+
+
+ GratiText is not directed at children under 13, and we do not
+ knowingly collect information from them. If you believe a child has
+ created an account, please contact us and we will remove it.
+
+
+
+
+
+ If we make meaningful changes to this policy we will update the date
+ at the top of this page and, where appropriate, let you know by text
+ or within the app.
+
+
+
+
+
+ Questions about privacy? Email us at{' '}
+ support@gratitext.app and a
+ human will get back to you.
+
+
+
)
}
diff --git a/app/routes/_app+/_marketing+/support.tsx b/app/routes/_app+/_marketing+/support.tsx
index 43472d8e..48671636 100644
--- a/app/routes/_app+/_marketing+/support.tsx
+++ b/app/routes/_app+/_marketing+/support.tsx
@@ -1,7 +1,151 @@
+import { Link, type MetaFunction } from 'react-router'
+import { ProsePage, ProseSection } from '#app/components/prose-page.tsx'
+import { Button } from '#app/components/ui/button.tsx'
+import { Icon } from '#app/components/ui/icon.tsx'
+
+export const meta: MetaFunction = () => [
+ { title: 'Support | GratiText' },
+ {
+ name: 'description',
+ content:
+ 'Get help with GratiText: answers to common questions and how to reach us.',
+ },
+]
+
+const faqs = [
+ {
+ question: 'Why has my recipient not received any messages yet?',
+ answer: (
+ <>
+ New recipients must confirm a verification code before messages begin.
+ Open the recipient, choose
Edit, and press{' '}
+
Verify to send the code. Ask your recipient to share
+ the code with you, then enter it to finish. Also make sure the
+ recipient's schedule is not paused and that at least one message is
+ waiting in their queue.
+ >
+ ),
+ },
+ {
+ question: 'How do schedules work?',
+ answer: (
+ <>
+ Each recipient has a schedule made of a day and time in their own time
+ zone. When that moment arrives, the oldest message in their queue is
+ sent. If the queue is empty about 30 minutes before a send, we text you
+ a reminder so you can add one.
+ >
+ ),
+ },
+ {
+ question: 'Can a recipient stop receiving messages?',
+ answer: (
+ <>
+ Yes. Any recipient can reply
STOP to opt out
+ immediately, and
START to opt back in. You can also
+ pause or delete a recipient from their settings page at any time.
+ >
+ ),
+ },
+ {
+ question: 'How do I change or cancel my plan?',
+ answer: (
+ <>
+ Go to
Subscription in
+ your settings. From there you can pick a plan or open the billing portal
+ to update your card, switch plans, or cancel. Your plan stays active
+ until the end of the current billing period.
+ >
+ ),
+ },
+ {
+ question: 'How do I change my phone number or password?',
+ answer: (
+ <>
+ Both live in
Settings. Changing your
+ number sends a confirmation code to the new number before it takes
+ effect.
+ >
+ ),
+ },
+ {
+ question: 'How do I export or delete my data?',
+ answer: (
+ <>
+ Open
Settings and choose{' '}
+
Download your data for a full JSON export, or{' '}
+
Delete account to remove everything permanently.
+ >
+ ),
+ },
+]
+
export default function SupportRoute() {
return (
-
-
Support
-
+
+
+
+
+
+
+
+
Email support
+
+ We usually reply within one business day.
+
+
+
+
+
+
+
+
+ Common questions
+
+
+ {faqs.map((faq) => (
+
+
+ {faq.question}
+
+
+
+ {faq.answer}
+
+
+ ))}
+
+
+
+
+
+ -
+ Why gratitude matters and how GratiText
+ fits into a daily habit.
+
+ -
+ Our Privacy Policy and{' '}
+ Terms of Service.
+
+
+
+
)
}
diff --git a/app/routes/_app+/_marketing+/tos.tsx b/app/routes/_app+/_marketing+/tos.tsx
index 211d56f4..8ff4e62a 100644
--- a/app/routes/_app+/_marketing+/tos.tsx
+++ b/app/routes/_app+/_marketing+/tos.tsx
@@ -1,7 +1,169 @@
+import { type MetaFunction } from 'react-router'
+import {
+ ProsePage,
+ ProseSection,
+ ProseTableOfContents,
+} from '#app/components/prose-page.tsx'
+
+export const meta: MetaFunction = () => [
+ { title: 'Terms of Service | GratiText' },
+ {
+ name: 'description',
+ content: 'The terms that govern your use of GratiText.',
+ },
+]
+
+const sections = [
+ { id: 'agreement', title: 'Agreement' },
+ { id: 'your-account', title: 'Your account' },
+ { id: 'recipients', title: 'Recipients and consent' },
+ { id: 'acceptable-use', title: 'Acceptable use' },
+ { id: 'billing', title: 'Trials, subscriptions, and billing' },
+ { id: 'delivery', title: 'Message delivery' },
+ { id: 'termination', title: 'Termination' },
+ { id: 'disclaimers', title: 'Disclaimers and liability' },
+ { id: 'changes', title: 'Changes to these terms' },
+ { id: 'contact', title: 'Contact' },
+]
+
export default function TermsOfServiceRoute() {
return (
-
-
Terms of Service
-
+
+
+
+
+
+ By creating an account or using GratiText you agree to these terms and
+ to our Privacy Policy. If you do not agree,
+ please do not use the service.
+
+
+
+
+
+ - You must be at least 18 years old to create an account.
+ -
+ You must provide a phone number you control and keep your login
+ details private. You are responsible for activity that happens under
+ your account.
+
+ -
+ Let us know right away at{' '}
+ support@gratitext.app if
+ you think your account has been compromised.
+
+
+
+
+
+
+ GratiText sends text messages on your behalf, so you are the sender of
+ every message. You agree that:
+
+
+ -
+ You only add people who know you and would welcome messages from
+ you.
+
+ -
+ New recipients confirm a verification code before your messages
+ begin, and you will respect their choice if they decline.
+
+ -
+ If a recipient replies STOP, their number is opted out and you will
+ not attempt to reach them through GratiText again unless they opt
+ back in.
+
+
+
+
+
+
+ GratiText is for gratitude and encouragement. You may not use it to:
+
+
+ - Send marketing, solicitations, or bulk messages of any kind.
+ - Harass, threaten, deceive, or impersonate anyone.
+ - Send content that is unlawful, hateful, or sexually explicit.
+ - Probe, disrupt, or reverse-engineer the service.
+
+
+ We may remove content or suspend accounts that violate these rules.
+
+
+
+
+
+ -
+ New accounts get a free trial. You will not be charged until you
+ choose a plan.
+
+ -
+ Plans are billed monthly in advance through Stripe. Prices are shown
+ on our pricing section and may change with
+ notice before your next billing period.
+
+ -
+ You can cancel at any time from the billing portal in your settings.
+ Your plan stays active until the end of the period you have already
+ paid for.
+
+ -
+ Plan limits (such as how many messages can be sent per day) apply as
+ described on the pricing page.
+
+
+
+
+
+
+ We work hard to deliver every message at the moment you scheduled it,
+ but text delivery ultimately depends on mobile carriers and networks
+ we do not control. Messages may occasionally be delayed or
+ undeliverable, and GratiText should not be relied on for urgent or
+ emergency communication.
+
+
+
+
+
+ You can delete your account whenever you like from your settings page.
+ We may suspend or terminate accounts that violate these terms or
+ create risk for other users. When an account is closed, scheduled
+ messages will no longer be sent.
+
+
+
+
+
+ GratiText is provided "as is" without warranties of any kind. To the
+ fullest extent permitted by law, GratiText and the people who build it
+ are not liable for indirect, incidental, or consequential damages, and
+ our total liability for any claim is limited to the amount you paid us
+ in the twelve months before the claim arose.
+
+
+
+
+
+ We may update these terms from time to time. When we do, we will
+ change the date at the top of this page and, for significant changes,
+ let you know by text or within the app. Continuing to use GratiText
+ after a change means you accept the updated terms.
+
+
+
+
+
+ Questions about these terms? Email{' '}
+ support@gratitext.app.
+
+
+
)
}
diff --git a/app/routes/_app+/recipients+/$recipientId.edit.tsx b/app/routes/_app+/recipients+/$recipientId.edit.tsx
index fc0d2f23..d1c87386 100644
--- a/app/routes/_app+/recipients+/$recipientId.edit.tsx
+++ b/app/routes/_app+/recipients+/$recipientId.edit.tsx
@@ -4,7 +4,10 @@ import {
type MetaFunction,
useLoaderData,
} from 'react-router'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import {
+ ErrorMessage,
+ GeneralErrorBoundary,
+} from '#app/components/error-boundary.tsx'
import { requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { RecipientEditor } from './__editor.tsx'
@@ -47,7 +50,7 @@ export const meta: MetaFunction
= ({ data }) => {
]
}
-export default function NoteEdit() {
+export default function RecipientEdit() {
const data = useLoaderData()
return (
@@ -63,7 +66,11 @@ export function ErrorBoundary() {
(
- No note with the id "{params.recipientId}" exists
+
),
}}
/>
diff --git a/app/routes/_app+/recipients+/$recipientId.index.tsx b/app/routes/_app+/recipients+/$recipientId.index.tsx
index 03a3ce45..2f5b4354 100644
--- a/app/routes/_app+/recipients+/$recipientId.index.tsx
+++ b/app/routes/_app+/recipients+/$recipientId.index.tsx
@@ -19,7 +19,10 @@ import {
useSearchParams,
} from 'react-router'
import { z } from 'zod'
-import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import {
+ ErrorMessage,
+ GeneralErrorBoundary,
+} from '#app/components/error-boundary.tsx'
import { ErrorList } from '#app/components/forms.js'
import { SearchBar } from '#app/components/search-bar.tsx'
import { Button } from '#app/components/ui/button.tsx'
@@ -596,16 +599,21 @@ export default function RecipientRoute() {