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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ web-build/
.env
.env.*
!.env.example
# Google Play service-account key (referenced by eas.json submit profile)
play-service-account.json
ios/
android/
.DS_Store
Expand Down
19 changes: 14 additions & 5 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# Privacy Policy

**Last updated: April 2026**
**Last updated: July 2026**

This is the whole policy. No dark patterns, no legalese.

Hosted copy (for app-store listings): <https://forage.techempower.org/privacy.html>
Account deletion requests: <https://forage.techempower.org/delete-account.html>

## What we collect

| Data | Why | Stored where | Retention |
|---|---|---|---|
| Email (if you sign up) | Magic-link auth only | InstantDB | Until you delete your account |
| Pins you create | The product | InstantDB | Until you delete them |
| Photos you upload | Shown on pins | InstantDB file storage | Until you delete them |
| Coarse device location (while app open) | To center the map | Device only, never sent | Session only |

(Photo upload is on the roadmap. When it ships, this table and the app-store
data-safety declarations get a "Photos you upload" row *first*.)

## What we do NOT collect

- Analytics. No Google Analytics, no PostHog, no Amplitude, no Mixpanel, no Segment.
Expand All @@ -25,7 +30,6 @@ This is the whole policy. No dark patterns, no legalese.

**Public** (anyone using the app can see):
- Pins you create (with fuzzy location ~110m by default)
- Photos on pins
- Your display name on pins and comments
- Ripeness confirmations you add to other pins

Expand All @@ -52,8 +56,13 @@ We have never sold and will never sell user data. If this project is ever transf

## Your rights

- **Export:** Settings → Export my data → downloadable JSON
- **Delete:** Settings → Delete account → wipes everything within 30 days
- **Delete:** Profile → Delete account (or visit
[forage.techempower.org/delete-account.html](https://forage.techempower.org/delete-account.html)).
Your account, pins, reports, comments, saves, and email are wiped within 30 days.
You can also delete any individual pin, report, or comment you created, directly in the app.
- **Export:** Email [jp@techempower.org](mailto:jp@techempower.org) with the subject
"Data export" from your account email — you'll get a JSON export within 30 days.
(A self-serve in-app export is on the roadmap.)
- **Correct:** Edit any pin or profile field directly

## Children
Expand Down
40 changes: 26 additions & 14 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,22 @@ const config: ExpoConfig = {
apiKey: process.env.GOOGLE_MAPS_ANDROID_KEY,
},
},
permissions: [
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"CAMERA",
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
// Play submission: request ONLY what the code actually uses today.
// Location (foreground, one-shot) centers the map and prefills the
// add-pin coordinate — see src/hooks/useCurrentLocation.ts. Coordinates
// are fuzzed to ~110m before any write (src/db/actions.ts).
permissions: ["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"],
// expo-camera and expo-image-picker are installed (photo upload is on
// the roadmap) but NO code path uses them yet. Their library manifests
// would still merge these permissions into the APK/AAB, which Play
// flags as unused sensitive permissions — block them until the photo
// feature actually ships. When it does: delete this list and restore
// the camera/image-picker config plugins below.
blockedPermissions: [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
],
},
plugins: [
Expand All @@ -74,15 +84,17 @@ const config: ExpoConfig = {
},
],
[
"expo-camera",
// Google Play requires new apps to target Android 15 (API 35) since
// 2025-08-31. Expo SDK 51 defaults to targetSdk 34, which Play
// rejects at AAB upload. compileSdk stays at the SDK 51 default (34)
// to remain inside AGP 8.2's support envelope. The real fix is the
// Expo SDK 52+ upgrade (tracked in docs/play-store/SUBMISSION-RUNBOOK.md);
// this override is the minimal change that makes the AAB uploadable.
"expo-build-properties",
{
cameraPermission: "Allow Forage to access your camera to document finds.",
},
],
[
"expo-image-picker",
{
photosPermission: "Allow Forage to access photos to attach to listings.",
android: {
targetSdkVersion: 35,
},
},
Comment on lines +93 to 98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Setting targetSdkVersion to 35 while keeping compileSdkVersion at 34 (the default for Expo SDK 51) will cause build failures on EAS. In Android development, compileSdkVersion must be greater than or equal to targetSdkVersion. Additionally, modern transitive dependencies (such as androidx.activity or androidx.recyclerview) will fail to compile if the app is compiled against SDK 34. Please set compileSdkVersion to 35 as well.

      "expo-build-properties",
      {
        android: {
          compileSdkVersion: 35,
          targetSdkVersion: 35,
        },
      },

],
],
Expand Down
16 changes: 15 additions & 1 deletion app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import React from "react";
import { ScrollView, View, StyleSheet, Pressable } from "react-native";
import { ScrollView, View, StyleSheet, Pressable, Linking } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
Expand Down Expand Up @@ -114,6 +114,20 @@ export default function ProfileScreen() {
icon={<Ionicons name="log-out-outline" size={18} color={colors.text} />}
/>
</View>
{/* Google Play requires an in-app account-deletion entry point
(linking out to the hosted deletion page is explicitly
allowed by the policy). Keep this URL in sync with
docs/delete-account.html and PRIVACY.md. */}
<View style={{ marginTop: spacing.sm }}>
<SecondaryButton
full
label="Delete account"
onPress={() =>
Linking.openURL("https://forage.techempower.org/delete-account.html")
}
Comment on lines +125 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Linking.openURL is an asynchronous operation that returns a Promise. If it fails (e.g., if the system fails to find an application to handle the URL), it will result in an unhandled promise rejection, which can cause instability or crashes. It is highly recommended to handle potential errors gracefully using a .catch() block.

                onPress={() =>
                  Linking.openURL("https://forage.techempower.org/delete-account.html").catch((err) =>
                    console.error("Failed to open URL:", err)
                  )
                }

icon={<Ionicons name="trash-outline" size={18} color={colors.text} />}
/>
</View>
</Section>
</ScrollView>
</SafeAreaView>
Expand Down
99 changes: 99 additions & 0 deletions docs/delete-account.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Delete your account · Forage for All</title>
<meta name="description" content="Request deletion of your Forage for All account and all associated data." />
<link rel="canonical" href="https://forage.techempower.org/delete-account.html" />
<meta name="theme-color" content="#F4EDDC" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><circle cx=%2250%22 cy=%2250%22 r=%2248%22 fill=%22%238FB36E%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22>🌿</text></svg>" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--cream: #F4EDDC; --paper: #FBF5E3; --soil: #3E2E1F; --ink: #1F1710;
--inkSoft: #5A4634; --inkMuted: #8B7457; --line: #D4C49E; --lineSoft: #E7D9B4;
--moss: #4A7C2E; --mossDeep: #2F5520; --mossLight: #8FB36E; --terra: #B8573A;
--serif: "Fraunces", "Iowan Old Style", Georgia, serif;
--sans: "Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--cream: #1D1710; --paper: #272016; --soil: #F0E7D3; --ink: #F4EDDC;
--inkSoft: #CDBFA6; --inkMuted: #9A886D; --line: #4A3D2C; --lineSoft: #382E20;
--moss: #8FB36E; --mossDeep: #A9C98C; --mossLight: #5C8A3C; --terra: #D97B5C;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: var(--sans); color: var(--ink); background: var(--cream); line-height: 1.6; }
a { color: var(--mossDeep); text-decoration: none; border-bottom: 1px solid var(--mossLight); }
a:hover { border-bottom-color: var(--mossDeep); }
h1, h2 { font-family: var(--serif); font-weight: 600; color: var(--soil); line-height: 1.15; letter-spacing: -0.015em; margin: 0; }
h1 { font-size: clamp(30px, 5vw, 42px); }
h2 { font-size: 22px; margin: 40px 0 12px; }
p, li { color: var(--inkSoft); }
header.nav { position: sticky; top: 0; background: color-mix(in srgb, var(--cream) 92%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--lineSoft); z-index: 5; }
.nav-inner { max-width: 760px; margin: 0 auto; padding: 14px 24px; display: flex; align-items: center; gap: 10px; }
.brand { display: flex; align-items: center; gap: 10px; font-family: var(--serif); font-weight: 700; font-size: 19px; border: 0; color: var(--soil); }
.brand-mark { width: 32px; height: 32px; border-radius: 50%; background: var(--mossLight); display: grid; place-items: center; font-size: 18px; }
main { max-width: 760px; margin: 0 auto; padding: 56px 24px 80px; }
ul, ol { padding-left: 22px; margin: 8px 0 16px; }
li { margin-bottom: 6px; }
.callout { background: var(--paper); border: 1px solid var(--lineSoft); border-radius: 12px; padding: 18px 20px; margin: 16px 0; }
.cta { display: inline-block; background: var(--moss); color: #FBF5E3; border: 0; border-radius: 999px; padding: 12px 22px; font-weight: 600; font-size: 15px; margin: 8px 0 4px; }
.cta:hover { background: var(--mossDeep); border: 0; color: #FBF5E3; }
footer { border-top: 1px solid var(--lineSoft); margin-top: 40px; padding-top: 24px; font-size: 14px; color: var(--inkMuted); }
strong { color: var(--soil); }
</style>
</head>
<body>
<header class="nav">
<div class="nav-inner">
<a class="brand" href="/"><span class="brand-mark">🌿</span> Forage for All</a>
</div>
</header>
<main>
<h1>Delete your account</h1>
<p>You can request deletion of your Forage for All account and all data associated with it
at any time. No questions, no retention tricks.</p>

<h2>What gets deleted</h2>
<ul>
<li>Your account and sign-in email</li>
<li>Your profile (handle, display name, badges)</li>
<li>Every pin you created</li>
<li>Your ripeness reports and comments</li>
<li>Your saved spots</li>
</ul>
<p>Deletion completes within <strong>30 days</strong> of the request. Nothing is retained afterward.</p>

<h2>How to request it</h2>
<ol>
<li><strong>In the app:</strong> Profile → <em>Delete account</em> (brings you to this page), or</li>
<li><strong>By email:</strong> send a message from your account email with the subject "Delete my account":</li>
</ol>
<p><a class="cta" href="mailto:jp@techempower.org?subject=Delete%20my%20account&body=Please%20delete%20my%20Forage%20for%20All%20account%20and%20all%20associated%20data.%20I%20am%20sending%20this%20from%20my%20account%20email.">Email a deletion request</a></p>
<p style="font-size:13.5px;color:var(--inkMuted)">We verify the request by matching the sender address
to the account email — that's all magic-code accounts have, so it's the strongest verification available.</p>

<h2>Deleting some data without closing your account</h2>
<p>You don't need to delete your account to remove content: any pin, ripeness report, or comment
you created can be deleted directly in the app, immediately and permanently.</p>

<div class="callout">
<p style="margin:0"><strong>Good to know:</strong> browsing the map never required an account in the
first place — accounts exist only so the community can trust who's confirming what's ripe.
Read the full <a href="/privacy.html">privacy policy</a>.</p>
</div>

<footer>
Forage for All is a free, open-source (AGPLv3) project by
<a href="https://techempower.org">TechEmpower</a>, a 501(c)(3) nonprofit.
· <a href="/">Home</a> · <a href="/privacy.html">Privacy policy</a>
</footer>
</main>
</body>
</html>
Loading
Loading